Skip to content

Commit

Permalink
Merge pull request #60 from Nexters/feature/sign-in-view(#19)
Browse files Browse the repository at this point in the history
[온보딩] 로그인 화면
  • Loading branch information
enebin authored Aug 21, 2023
2 parents ac469a3 + fd23dfd commit 17f08de
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 19 deletions.
4 changes: 4 additions & 0 deletions Keyme.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array/>
</dict>
Expand Down
17 changes: 9 additions & 8 deletions Projects/Features/Sources/Root/RootFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// Features
//
// Created by 이영빈 on 2023/08/10.
// Edited by 고도 on 2023/08/14.
//
// Copyright © 2023 team.humanwave. All rights reserved.
//

Expand Down Expand Up @@ -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)):
Expand Down
126 changes: 123 additions & 3 deletions Projects/Features/Sources/SignIn/SignInFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,147 @@
// 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
case loggedOut
}

public enum Action: Equatable {
case succeeded
case failed
case signInWithKakao
case signInWithKakaoResponse(TaskResult<Bool>)

case signInWithApple(AppleOAuthResponse)
case signInWithAppleResponse(TaskResult<Bool>)
// case succeeded
// case failed
}

public var body: some Reducer<State, Action> {
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
}
}
}
106 changes: 102 additions & 4 deletions Projects/Features/Sources/SignIn/SignInView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<SignInFeature>

Expand All @@ -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<SignInFeature>

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<SignInFeature>

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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions Projects/Keyme/Sources/KeymeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
}
Expand Down
Loading

0 comments on commit 17f08de

Please sign in to comment.