Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ jobs:
-configuration Debug \
-skipMacroValidation \
-destination "generic/platform=iOS Simulator" \
clean build
SWIFT_ENABLE_EXPLICIT_MODULES=NO \
build

- name: Run unit tests (all *Tests schemes)
run: |
Expand Down Expand Up @@ -88,5 +89,6 @@ jobs:
-configuration Debug \
-skipMacroValidation \
-destination "id=${SIMULATOR_ID}" \
SWIFT_ENABLE_EXPLICIT_MODULES=NO \
test
done <<< "${TEST_SCHEMES}"
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 @@ -69,6 +69,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