Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Projects/App/DoriAppDebug.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
</dict>
</plist>
4 changes: 4 additions & 0 deletions Projects/App/Resources/DoriApp.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
</dict>
</plist>
4 changes: 4 additions & 0 deletions Projects/App/Sources/DoriApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ struct DoriApp: App {
networkService: networkService,
tokenStore: tokenStore
)
$0.appleServerLoginClient = .live(
networkService: networkService,
tokenStore: tokenStore
)

$0.addDoriAPIClient = .live(networkService: networkService)
$0.calendarClient = .live(networkService: networkService)
Expand Down
92 changes: 92 additions & 0 deletions Projects/Feature/Onboarding/Sources/AppleServerLoginClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// AppleServerLoginClient.swift
// Dori-iOS
//
// Created by 강동영 on 6/25/26.
//

import Foundation
import ComposableArchitecture
import DoriNetwork

public struct AppleServerLoginClient: Sendable {
public var login: @Sendable (_ identityToken: String, _ user: AppleLoginUserInfo?) async throws -> SocialLoginResponse

public init(login: @escaping @Sendable (_ identityToken: String, _ user: AppleLoginUserInfo?) async throws -> SocialLoginResponse) {
self.login = login
}
}

private enum AppleServerLoginClientError: LocalizedError {
case unconfigured
case invalidResponse
case backendError(String)

var errorDescription: String? {
switch self {
case .unconfigured:
return "AppleServerLoginClient가 구성되지 않았습니다."
case .invalidResponse:
return "서버 응답이 올바르지 않습니다."
case .backendError(let message):
return message
}
}
}

extension AppleServerLoginClient: DependencyKey {
public static let liveValue = Self(
login: { _, _ in
throw AppleServerLoginClientError.unconfigured
}
)

public static let testValue = Self(
login: { _, _ in
SocialLoginResponse(
accessToken: "test-jwt",
refreshToken: "test-refresh",
id: 1
)
}
)
}

public extension DependencyValues {
var appleServerLoginClient: AppleServerLoginClient {
get { self[AppleServerLoginClient.self] }
set { self[AppleServerLoginClient.self] = newValue }
}
}

public extension AppleServerLoginClient {
static func live(
networkService: any NetworkService,
tokenStore: any AuthTokenStoring
) -> Self {
Self(
login: { identityToken, user in
let endpoint = AppleLoginEndpoint(identityToken: identityToken, user: user)
let response = try await networkService.request(
endpoint,
responseType: SuccessResponse<SocialLoginResponse>.self
)

if let apiError = response.error {
throw AppleServerLoginClientError.backendError(
apiError.message ?? apiError.code
)
}

guard response.success, let data = response.data else {
throw AppleServerLoginClientError.invalidResponse
}

print("token save accessToken: \(data.accessToken)")
print("token save refreshToken: \(data.refreshToken)")
try tokenStore.save(accessToken: data.accessToken, refreshToken: data.refreshToken)
return data
}
)
}
}
43 changes: 43 additions & 0 deletions Projects/Feature/Onboarding/Sources/IntroFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import PlatformKakaoAuth
import PlatformAppleAuth
import DoriNetwork
import ComposableArchitecture

Expand All @@ -26,6 +27,8 @@ public struct IntroFeature : Sendable {
public enum Action: Equatable, Sendable {
case kakaoLoginButtonTapped
case kakaoLoginResponse(Result<SocialLoginResponse, KakaoLoginFailure>)
case appleLoginButtonTapped
case appleLoginResponse(Result<SocialLoginResponse, AppleLoginFailure>)
case errorAlertDismissed
case delegate(Delegate)

Expand All @@ -36,6 +39,8 @@ public struct IntroFeature : Sendable {

@Dependency(KakaoAuthClient.self) var kakaoAuthClient
@Dependency(KakaoServerLoginClient.self) var kakaoServerLoginClient
@Dependency(AppleAuthClient.self) var appleAuthClient
@Dependency(AppleServerLoginClient.self) var appleServerLoginClient

public func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
Expand Down Expand Up @@ -64,6 +69,35 @@ public struct IntroFeature : Sendable {
state.errorMessage = error.message
return .none

case .appleLoginButtonTapped:
guard !state.isLoading else { return .none }
state.isLoading = true
state.errorMessage = nil
return .run { send in
do {
let credential = try await appleAuthClient.login()
let user = AppleLoginUserInfo(
firstName: credential.firstName,
lastName: credential.lastName,
email: credential.email
)
let loginResponse = try await appleServerLoginClient.login(credential.identityToken, user)
await send(.appleLoginResponse(.success(loginResponse)))
} catch {
await send(.appleLoginResponse(.failure(AppleLoginFailure(error))))
}
}

case .appleLoginResponse(.success):
state.isLoading = false
state.loginSucceeded = true
return .send(.delegate(.loginSucceeded))

case let .appleLoginResponse(.failure(error)):
state.isLoading = false
state.errorMessage = error.message
return .none

case .errorAlertDismissed:
state.errorMessage = nil
return .none
Expand All @@ -82,3 +116,12 @@ public struct KakaoLoginFailure: Error, Equatable, Sendable {
self.message = description.isEmpty ? "로그인 중 오류가 발생했습니다." : description
}
}

public struct AppleLoginFailure: Error, Equatable, Sendable {
public let message: String

public init(_ error: Error) {
let description = (error as NSError).localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
self.message = description.isEmpty ? "로그인 중 오류가 발생했습니다." : description
}
}
43 changes: 41 additions & 2 deletions Projects/Feature/Onboarding/Sources/IntroView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ public struct IntroView: View {
Spacer()

kakaoLoginButton
appleLoginButton
}
.alert(
"카카오 로그인 실패",
"로그인 실패",
isPresented: Binding(
get: { store.errorMessage != nil },
set: { isPresented in
Expand Down Expand Up @@ -166,7 +167,45 @@ public struct IntroView: View {
}
}
.disabled(store.isLoading)
.padding()
.padding(.horizontal, 16)
.padding(.top, 16)
.padding(.bottom, 8)
}

var appleLoginButton: some View {
Button {
store.send(.appleLoginButtonTapped)
} label: {
Text("Apple로 시작하기")
.pretendard(.semiBold(.sb15))
.foregroundStyle(.bgPrimary)
.frame(maxWidth: .infinity)
.frame(height: 46)
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.textPrimary)
)
.overlay() {
HStack {
Image(systemName: "apple.logo")
.resizable()
.scaledToFit()
.foregroundStyle(.bgPrimary)
.frame(
width: 24,
height: 24
)
.padding(.leading, 16)

Spacer()
}

}
}
.disabled(store.isLoading)
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 32)
}
}

Expand Down
Loading
Loading