Skip to content

Commit

Permalink
[Feat] - DDD-Community#44 애플 로그인, 카카오 로그인 로직 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
sunny5875 committed Jan 15, 2025
1 parent 342bc54 commit 9bf33a6
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 19 deletions.
16 changes: 10 additions & 6 deletions Projects/App/Entitlements/OnboardingKit.entitlements
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)group.com.DDD.OnboardingKit</string>
</array>
</dict>
<dict>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)group.com.DDD.OnboardingKit</string>
</array>
</dict>
</plist>
31 changes: 31 additions & 0 deletions Projects/Core/Domain/Sources/Models/SocialLoginInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// SocialLoginInfo.swift
// CoreDomain
//
// Created by 현수빈 on 1/15/25.
//

import Foundation

public struct SocialLoginInfo: Equatable {
public let idToken: String
public let nonce: String?
public let provider: Socialtype

public init(
idToken: String,
nonce: String? = nil,
provider: Socialtype
) {
self.idToken = idToken
self.nonce = nonce
self.provider = provider
}
}

extension SocialLoginInfo {
public enum Socialtype: String {
case kakao = "Kakao"
case apple = "Apple"
}
}
93 changes: 93 additions & 0 deletions Projects/Core/Network/Sources/Services/Login/AppleLogin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// SocialLogin.swift
// CoreNetwork
//
// Created by 현수빈 on 1/15/25.
//

import AuthenticationServices
import Foundation

import CoreDomain

public enum AppleErrorType: Error {
case invalidToken
case invalidAuthorizationCode
case dismissASAuthorizationController
}

final class AppleLogin: NSObject, ASAuthorizationControllerDelegate {
private var continuation: CheckedContinuation<SocialLoginInfo, Error>? = nil

/// 애플 로그인
@MainActor
func appleLogin() async throws -> SocialLoginInfo {
return try await withCheckedThrowingContinuation { continuation in
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]

let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.performRequests()

if self.continuation == nil {
self.continuation = continuation
}
}
}

func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
let email = appleIDCredential.email
debugPrint("appleLogin email: \(email ?? "")")
let fullName = appleIDCredential.fullName
debugPrint("appleLogin fullName: \(fullName?.description ?? "")")

guard let tokenData = appleIDCredential.identityToken,
let token = String(data: tokenData, encoding: .utf8) else {
continuation?.resume(throwing: AppleErrorType.invalidToken)
continuation = nil
return
}

debugPrint("appleLogin token: \(token)")

guard let authorizationCode = appleIDCredential.authorizationCode,
let authorizationCodeString = String(data: authorizationCode, encoding: .utf8) else {
continuation?.resume(throwing: AppleErrorType.invalidAuthorizationCode)
continuation = nil
return
}

debugPrint("appleLogin authorizationCode: \(authorizationCodeString)")

let userIdentifier = appleIDCredential.user
debugPrint("appleLogin authenticated user: \(userIdentifier)")

let info = SocialLoginInfo(idToken: token, provider: .apple)

continuation?.resume(returning: info)
continuation = nil

default:
break
}
}

@MainActor
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
if let authError = error as? ASAuthorizationError {
switch authError.code {
case .canceled:
continuation?.resume(throwing: AppleErrorType.dismissASAuthorizationController)
continuation = nil

default:
continuation?.resume(throwing: authError)
continuation = nil
}
}
}
}
98 changes: 98 additions & 0 deletions Projects/Core/Network/Sources/Services/Login/KakaoLogin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// KakaoLogin.swift
// CoreNetwork
//
// Created by 현수빈 on 1/15/25.
//

import Foundation

import CoreDomain

import KakaoSDKAuth
import KakaoSDKCommon
import KakaoSDKUser

enum KakaoErrorType: Error {
case invalidToken
}

final class KakaoLogin {
private var continuation: CheckedContinuation<SocialLoginInfo, Error>? = nil


func initSDK() {
if let appkey = Bundle.main.infoDictionary?["KAKAO_NATIVE_APP_KEY"] as? String {
KakaoSDK.initSDK(appKey: appkey)
}
}

/// Handle KakaoTalkLoginUrl
func handleKakaoTalkLoginUrl(url: URL) {
guard AuthApi.isKakaoTalkLoginUrl(url) else { return }
_ = AuthController.handleOpenUrl(url: url)
}

/// 카카오톡 로그인
@MainActor
func kakaoLogin() async throws -> SocialLoginInfo {
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
let nonce = UUID().uuidString

if UserApi.isKakaoTalkLoginAvailable() {
loginWithKakaoTalk(nonce: nonce)
} else {
loginWithKakaoWeb(nonce: nonce)
}
}
}

/// 카카오톡(앱)으로 로그인
private func loginWithKakaoTalk(nonce: String) {
UserApi.shared.loginWithKakaoTalk(nonce: nonce) { [weak self] OAuthToken, error in
guard let self else { return }

if let error {
self.continuation?.resume(throwing: error)
self.continuation = nil
debugPrint("\(error)")
return
} else if let token = OAuthToken?.idToken {
self.setSocialLoginData(idToken: token, nonce: nonce)
debugPrint("loginWithKakaoTalk() success., \(#function), \(#line)")
} else {
self.continuation?.resume(throwing: KakaoErrorType.invalidToken)
self.continuation = nil
return
}
}
}

/// 웹에서 카카오 계정 Access
private func loginWithKakaoWeb(nonce: String) {
UserApi.shared.loginWithKakaoAccount(nonce: nonce) { [weak self] OAuthToken, error in
guard let self else { return }

if let error {
self.continuation?.resume(throwing: error)
self.continuation = nil
debugPrint("\(error)")
return
} else if let token = OAuthToken?.idToken {
self.setSocialLoginData(idToken: token, nonce: nonce)
debugPrint("loginWithWeb() success., \(#function), \(#line)")
} else {
self.continuation?.resume(throwing: KakaoErrorType.invalidToken)
self.continuation = nil
return
}
}
}

private func setSocialLoginData(idToken: String, nonce: String) {
let info = SocialLoginInfo(idToken: idToken, nonce: nonce, provider: .kakao)
continuation?.resume(returning: info)
continuation = nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// SocialLoginClient.swift
// CoreNetwork
//
// Created by 현수빈 on 1/15/25.
//

import Foundation

import CoreDomain

import ComposableArchitecture

public struct SocialLoginClient {
public var initKakaoSDK: @Sendable () -> Void
public var handleKakaoUrl: @Sendable (URL) -> Void
public var kakaoLogin: @Sendable () async throws -> SocialLoginInfo
public var appleLogin: @Sendable () async throws -> SocialLoginInfo
}

extension SocialLoginClient: DependencyKey {
public static var liveValue: SocialLoginClient {
let kakaoLogin = KakaoLogin()
let appleLogin = AppleLogin()

return Self(
initKakaoSDK: {
kakaoLogin.initSDK()
},
handleKakaoUrl: {
kakaoLogin.handleKakaoTalkLoginUrl(url: $0)
},
kakaoLogin: {
try await kakaoLogin.kakaoLogin()
},
appleLogin: {
try await appleLogin.appleLogin()
}
)
}
}

public extension DependencyValues {
var socialLogin: SocialLoginClient {
get { self[SocialLoginClient.self] }
set { self[SocialLoginClient.self] = newValue }
}
}
18 changes: 18 additions & 0 deletions Projects/Core/Network/Sources/Services/Login/TokenInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// TokenInfo.swift
// CoreNetwork
//
// Created by 현수빈 on 1/15/25.
//

import Foundation

public struct TokenInfo {
public let accessToken: String
public let refreshToken: String

public init(accessToken: String, refreshToken: String) {
self.accessToken = accessToken
self.refreshToken = refreshToken
}
}
Loading

0 comments on commit 9bf33a6

Please sign in to comment.