diff --git a/Keyme.entitlements b/Keyme.entitlements
index cc78b75e..9ff04609 100644
--- a/Keyme.entitlements
+++ b/Keyme.entitlements
@@ -4,6 +4,10 @@
aps-environment
development
+ com.apple.developer.applesignin
+
+ Default
+
com.apple.developer.associated-domains
diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift
index ca100073..b4ebceb9 100644
--- a/Projects/Features/Sources/Root/RootFeature.swift
+++ b/Projects/Features/Sources/Root/RootFeature.swift
@@ -3,6 +3,8 @@
// Features
//
// Created by 이영빈 on 2023/08/10.
+// Edited by 고도 on 2023/08/14.
+//
// Copyright © 2023 team.humanwave. All rights reserved.
//
@@ -54,14 +56,13 @@ public struct RootFeature: Reducer {
Reduce { state, action in
switch action {
case .login(.presented(let result)):
- switch result {
- case .succeeded:
- localStorage.set(true, forKey: .isLoggedIn)
- state.logInStatus = .loggedIn
- case .failed:
- localStorage.set(false, forKey: .isLoggedIn)
- state.logInStatus = .loggedOut
- }
+// switch result {
+// case .succeeded:
+// localStorage.set(true, forKey: .isLoggedIn)
+// state.logInStatus = .loggedIn
+// case .failed:
+// localStorage.set(false, forKey: .isLoggedIn)
+// state.logInStatus = .loggedOut
return .none
case .onboarding(.presented(let result)):
diff --git a/Projects/Features/Sources/SignIn/SignInFeature.swift b/Projects/Features/Sources/SignIn/SignInFeature.swift
index 712f8b08..d05106f9 100644
--- a/Projects/Features/Sources/SignIn/SignInFeature.swift
+++ b/Projects/Features/Sources/SignIn/SignInFeature.swift
@@ -3,13 +3,24 @@
// Features
//
// Created by 이영빈 on 2023/08/10.
+// Edited by 고도 on 2023/08/14.
+//
// Copyright © 2023 team.humanwave. All rights reserved.
//
-import Foundation
+import AuthenticationServices
import ComposableArchitecture
+import Foundation
+import KakaoSDKUser
+import Network
+
+public enum SignInError: Error {
+ case noSignIn
+}
public struct SignInFeature: Reducer {
+ @Dependency(\.localStorage) private var localStorage
+
public enum State: Equatable {
case notDetermined
case loggedIn
@@ -17,13 +28,122 @@ public struct SignInFeature: Reducer {
}
public enum Action: Equatable {
- case succeeded
- case failed
+ case signInWithKakao
+ case signInWithKakaoResponse(TaskResult)
+
+ case signInWithApple(AppleOAuthResponse)
+ case signInWithAppleResponse(TaskResult)
+ // case succeeded
+ // case failed
}
public var body: some Reducer {
Reduce { state, action in
+ switch action {
+ case .signInWithKakao:
+ return .run { send in
+ await send(.signInWithKakaoResponse(
+ TaskResult {
+ try await signInWithKakao()
+ }
+ ))
+ }
+
+ case .signInWithKakaoResponse(.success(true)): // 카카오 로그인 성공
+ state = .loggedIn
+ localStorage.set(true, forKey: .isLoggedIn)
+ return .none
+
+ case .signInWithKakaoResponse(.failure): // 카카오 로그인 실패
+ state = .loggedOut
+ return .none
+
+ case .signInWithApple(let appleOAuth):
+ return .run { send in
+ await send(.signInWithAppleResponse(
+ TaskResult {
+ await signInWithApple(appleOAuth)
+ }
+ ))
+ }
+
+ case .signInWithAppleResponse(.success(true)): // 애플 로그인 성공
+ state = .loggedIn
+ localStorage.set(true, forKey: .isLoggedIn)
+ return .none
+
+ case .signInWithAppleResponse(.failure): // 애플 로그인 실패
+ state = .loggedOut
+ return .none
+
+ default:
+ state = .loggedOut
+ }
+
return .none
}
}
}
+
+extension SignInFeature {
+ // 카카오 로그인 메서드
+ /// Reducer Closure 내부에서 State를 직접 변경할 수 없어서 Async - Await를 활용하여 한 번 더 이벤트(signInWithKakaoResponse)를 발생시키도록 구현했습니다.
+ private func signInWithKakao() async throws -> Bool {
+ return try await withCheckedThrowingContinuation { continuation in
+ if (UserApi.isKakaoTalkLoginAvailable()) {
+ UserApi.shared.loginWithKakaoTalk { (token, error) in
+ if let error = error {
+ continuation.resume(throwing: SignInError.noSignIn)
+ } else {
+ continuation.resume(returning: true)
+ }
+ }
+ } else {
+ UserApi.shared.loginWithKakaoAccount() { (data, error) in
+ if let error = error {
+ continuation.resume(throwing: SignInError.noSignIn)
+ } else {
+ do {
+ // 1. 카카오 API로 사용자 정보 가져오기
+ let jsonData = try JSONEncoder().encode(data)
+ let parsedData = try JSONDecoder().decode(KakaoOAuthResponse.self, from: jsonData)
+
+ // 2. Keyme API로 사용자 토큰 확인하기
+ Task {
+ do {
+ let auth = KeymeOAuthRequest(oauthType: "KAKAO", token: parsedData.accessToken)
+ let result = try await KeymeAPIManager.shared.request(.auth(param: auth), object: KeymeOAuthResponse.self)
+
+ if result.code == 200 {
+ return continuation.resume(returning: true)
+ } else {
+ return continuation.resume(throwing: SignInError.noSignIn)
+ }
+ } catch { // 에러가 발생하면 실패 처리
+ return continuation.resume(throwing: SignInError.noSignIn)
+ }
+ }
+ } catch { // 에러가 발생하면 실패 처리
+ continuation.resume(throwing: SignInError.noSignIn)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private func signInWithApple(_ appleOAuth: AppleOAuthResponse) async -> Bool {
+ do {
+ let auth = KeymeOAuthRequest(oauthType: "APPLE", token: appleOAuth.identifyToken!) // FIXME: 강제 언래핑
+ let result = try await KeymeAPIManager.shared.request(.auth(param: auth), object: KeymeOAuthResponse.self)
+
+ if result.code == 200 {
+ return true
+ } else {
+ return false
+ }
+ } catch {
+ return false
+ }
+ }
+}
diff --git a/Projects/Features/Sources/SignIn/SignInView.swift b/Projects/Features/Sources/SignIn/SignInView.swift
index 11a2f082..faba2c76 100644
--- a/Projects/Features/Sources/SignIn/SignInView.swift
+++ b/Projects/Features/Sources/SignIn/SignInView.swift
@@ -3,13 +3,16 @@
// Keyme
//
// Created by 이영빈 on 2023/08/10.
+// Edited by 고도 on 2023/08/14.
+//
// Copyright © 2023 team.humanwave. All rights reserved.
//
-import SwiftUI
+import AuthenticationServices
import ComposableArchitecture
+import SwiftUI
+import Network
-// FIXME: Temp
public struct SignInView: View {
private let store: StoreOf
@@ -18,8 +21,103 @@ public struct SignInView: View {
}
public var body: some View {
- Button(action: { store.send(.succeeded) }) {
- Text("로그인?")
+ VStack(alignment: .center, spacing: 0) {
+ Spacer()
+
+ KakaoLoginButton(store: store)
+
+ AppleLoginButton(store: store)
+
+ GuideMessageView()
+ }
+ .padding()
+ }
+
+ // 카카오 로그인 버튼
+ struct KakaoLoginButton: View {
+ let store: StoreOf
+
+ var body: some View {
+ Button(action: {
+ store.send(.signInWithKakao)
+ }) {
+ Image("kakao_login")
+ .resizable()
+ .scaledToFill()
+ }
+ .frame(width: 312, height: 48)
+ .cornerRadius(6)
+ }
+ }
+
+ // 애플 로그인 버튼
+ struct AppleLoginButton: View {
+ let store: StoreOf
+
+ var body: some View {
+ SignInWithAppleButton(
+ onRequest: { request in
+ request.requestedScopes = [.fullName, .email]
+ },
+ onCompletion: { completion in
+ switch completion {
+ case .success(let response):
+ switch response.credential { // FIXME: 추후에 SignInFeature으로 이동
+ case let appleIDCredential as ASAuthorizationAppleIDCredential:
+ let user = appleIDCredential.user
+ let fullName = appleIDCredential.fullName
+ let name = (fullName?.familyName ?? "") + (fullName?.givenName ?? "")
+ let email = appleIDCredential.email
+ let identifyToken = String(data: appleIDCredential.identityToken!, encoding: .utf8)
+ let authorizationCode = String(data: appleIDCredential.authorizationCode!, encoding: .utf8)
+ let appleOAuth = AppleOAuthResponse(user: user,
+ fullName: fullName,
+ name: name,
+ email: email,
+ identifyToken: identifyToken,
+ authorizationCode: authorizationCode)
+ store.send(.signInWithApple(appleOAuth))
+ default:
+ store.send(.signInWithAppleResponse(.failure(SignInError.noSignIn)))
+ }
+ case .failure:
+ store.send(.signInWithAppleResponse(.failure(SignInError.noSignIn)))
+ }
+
+ })
+ .signInWithAppleButtonStyle(.white)
+ .frame(width: 312, height: 48)
+ .cornerRadius(6)
+ .padding(.vertical)
+ }
+ }
+
+ struct GuideMessageView: View {
+ var body: some View {
+ VStack(spacing: 8) {
+ Text("가입 시, 키미의 다음 사항에 동의하는 것으로 간주합니다.")
+ .foregroundColor(.gray)
+
+ HStack(spacing: 4) {
+ Button(action: {}) {
+ Text("서비스 이용약관")
+ .fontWeight(.bold)
+ .foregroundColor(.white)
+ }
+
+ Text("및")
+ .foregroundColor(.gray)
+
+ Button(action: {}) {
+ Text("개인정보 정책")
+ .fontWeight(.bold)
+ .foregroundColor(.white)
+ }
+ .foregroundColor(.white)
+ }
+ }
+ .font(.system(size: 11))
+ .frame(width: 265, height: 36)
}
}
}
diff --git a/Projects/Keyme/Resources/Assets.xcassets/apple_login_white.imageset/Contents.json b/Projects/Keyme/Resources/Assets.xcassets/apple_login_white.imageset/Contents.json
new file mode 100644
index 00000000..60958d61
--- /dev/null
+++ b/Projects/Keyme/Resources/Assets.xcassets/apple_login_white.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "apple_login_white.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Projects/Keyme/Resources/Assets.xcassets/apple_login_white.imageset/apple_login_white.png b/Projects/Keyme/Resources/Assets.xcassets/apple_login_white.imageset/apple_login_white.png
new file mode 100644
index 00000000..5f33bcf8
Binary files /dev/null and b/Projects/Keyme/Resources/Assets.xcassets/apple_login_white.imageset/apple_login_white.png differ
diff --git a/Projects/Keyme/Resources/Assets.xcassets/kakao_login.imageset/Contents.json b/Projects/Keyme/Resources/Assets.xcassets/kakao_login.imageset/Contents.json
new file mode 100644
index 00000000..5eb4ba1b
--- /dev/null
+++ b/Projects/Keyme/Resources/Assets.xcassets/kakao_login.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "kakao_login.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Projects/Keyme/Resources/Assets.xcassets/kakao_login.imageset/kakao_login.png b/Projects/Keyme/Resources/Assets.xcassets/kakao_login.imageset/kakao_login.png
new file mode 100644
index 00000000..c0c18561
Binary files /dev/null and b/Projects/Keyme/Resources/Assets.xcassets/kakao_login.imageset/kakao_login.png differ
diff --git a/Projects/Keyme/Sources/KeymeApp.swift b/Projects/Keyme/Sources/KeymeApp.swift
index e1a614b5..5f575c85 100644
--- a/Projects/Keyme/Sources/KeymeApp.swift
+++ b/Projects/Keyme/Sources/KeymeApp.swift
@@ -8,13 +8,27 @@ import FirebaseMessaging
import Features
import Network
+import KakaoSDKAuth
+import KakaoSDKCommon
+
@main
struct KeymeApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+ let KAKAO_PRIVATE_KEY = "" // 🚨 SECRET 🚨
+
+ init() {
+ KakaoSDK.initSDK(appKey: KAKAO_PRIVATE_KEY)
+ }
+
var body: some Scene {
WindowGroup {
RootView()
+ .onOpenURL(perform: { url in
+ if (AuthApi.isKakaoTalkLoginUrl(url)) {
+ AuthController.handleOpenUrl(url: url)
+ }
+ })
}
}
}
diff --git a/Projects/Network/Sources/DTO/Auth.swift b/Projects/Network/Sources/DTO/Auth.swift
new file mode 100644
index 00000000..ca034b67
--- /dev/null
+++ b/Projects/Network/Sources/DTO/Auth.swift
@@ -0,0 +1,85 @@
+//
+// Auth.swift
+// Network
+//
+// Created by 고도현 on 2023/08/15.
+// Copyright © 2023 team.humanwave. All rights reserved.
+//
+
+import Foundation
+
+public struct KakaoOAuthResponse: Codable { // 카카오 API Response 관련 Model
+ public let refreshTokenExpiresIn: Int
+ public let tokenType: String
+ public let refreshToken: String
+ public let accessToken: String
+ public let expiresIn: Int
+ public let scope: String
+
+ public init(refreshTokenExpiresIn: Int,
+ tokenType: String,
+ refreshToken: String,
+ accessToken: String,
+ expiresIn: Int,
+ scope: String) {
+ self.refreshTokenExpiresIn = refreshTokenExpiresIn
+ self.tokenType = tokenType
+ self.refreshToken = refreshToken
+ self.accessToken = accessToken
+ self.expiresIn = expiresIn
+ self.scope = scope
+ }
+}
+
+public struct AppleOAuthResponse: Codable, Equatable { // 애플 API Response 관련 Model
+ public let user: String
+ public let fullName: PersonNameComponents?
+ public let name: String
+ public let email: String?
+ public let identifyToken: String?
+ public let authorizationCode: String?
+
+ public init(user: String,
+ fullName: PersonNameComponents?,
+ name: String,
+ email: String?,
+ identifyToken: String?,
+ authorizationCode: String?) {
+ self.user = user
+ self.fullName = fullName
+ self.name = name
+ self.email = email
+ self.identifyToken = identifyToken
+ self.authorizationCode = authorizationCode
+ }
+}
+
+public struct KeymeOAuthRequest: Codable { // KeyMe API Request 관련 Model
+ public let oauthType: String
+ public let token: String
+
+ public init(oauthType: String,
+ token: String) {
+ self.oauthType = oauthType
+ self.token = token
+ }
+}
+
+public struct KeymeOAuthResponse: Codable { // KeyMe API Response 관련 Model
+ public let code: Int // statusCode
+ public let data: Data
+ public let message: String
+
+ public struct Data: Codable {
+ public let id: Int
+ public let friendCode: String?
+ public let nickname: String?
+ public let profileImage: String?
+ public let profileTumbnail: String?
+ public let token: Token
+
+ public struct Token: Codable {
+ public let accessToken: String
+ }
+ }
+}
diff --git a/Projects/Network/Sources/Network/Foundation/KeymeAPI.swift b/Projects/Network/Sources/Network/Foundation/KeymeAPI.swift
index 37b667f3..80c39ef1 100644
--- a/Projects/Network/Sources/Network/Foundation/KeymeAPI.swift
+++ b/Projects/Network/Sources/Network/Foundation/KeymeAPI.swift
@@ -14,6 +14,7 @@ public enum KeymeAPI {
case test
case myPage(MyPage)
case registerPushToken(String)
+ case auth(param: KeymeOAuthRequest)
}
public enum MyPage {
@@ -38,9 +39,11 @@ extension KeymeAPI: BaseAPI {
return "/members/\(id)/statistics"
case .registerPushToken:
return "/members/devices"
+ case .auth:
+ return "/auth/login"
}
}
-
+
public var method: Moya.Method {
switch self {
case .test:
@@ -49,9 +52,11 @@ extension KeymeAPI: BaseAPI {
return .get
case .registerPushToken:
return .post
+ case .auth:
+ return .post
}
}
-
+
public var task: Task {
switch self {
case .test:
@@ -60,13 +65,15 @@ extension KeymeAPI: BaseAPI {
return .requestParameters(parameters: ["type": type.rawValue], encoding: URLEncoding.default)
case .registerPushToken(let token):
return .requestParameters(parameters: ["token": token], encoding: JSONEncoding.default)
+ case .auth(let param):
+ return .requestJSONEncodable(param)
}
}
-
+
public var headers: [String: String]? {
return ["Content-type": "application/json"]
}
-
+
public var sampleData: Data {
"""
{
diff --git a/Tuist/Signing/Keyme.DEV.mobileprovision b/Tuist/Signing/Keyme.DEV.mobileprovision
index de476695..ea85df2b 100644
Binary files a/Tuist/Signing/Keyme.DEV.mobileprovision and b/Tuist/Signing/Keyme.DEV.mobileprovision differ
diff --git a/Tuist/Signing/Keyme.PROD.mobileprovision b/Tuist/Signing/Keyme.PROD.mobileprovision
index 7f2b7d09..79880aae 100644
Binary files a/Tuist/Signing/Keyme.PROD.mobileprovision and b/Tuist/Signing/Keyme.PROD.mobileprovision differ