diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0139367..18542ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: | @@ -88,5 +89,6 @@ jobs: -configuration Debug \ -skipMacroValidation \ -destination "id=${SIMULATOR_ID}" \ + SWIFT_ENABLE_EXPLICIT_MODULES=NO \ test done <<< "${TEST_SCHEMES}" diff --git a/Projects/App/DoriAppDebug.entitlements b/Projects/App/DoriAppDebug.entitlements index 903def2..80b5221 100644 --- a/Projects/App/DoriAppDebug.entitlements +++ b/Projects/App/DoriAppDebug.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.developer.applesignin + + Default + diff --git a/Projects/App/Resources/DoriApp.entitlements b/Projects/App/Resources/DoriApp.entitlements index 903def2..80b5221 100644 --- a/Projects/App/Resources/DoriApp.entitlements +++ b/Projects/App/Resources/DoriApp.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.developer.applesignin + + Default + diff --git a/Projects/App/Sources/DoriApp.swift b/Projects/App/Sources/DoriApp.swift index 1368034..d0dc7f8 100644 --- a/Projects/App/Sources/DoriApp.swift +++ b/Projects/App/Sources/DoriApp.swift @@ -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) diff --git a/Projects/Feature/Onboarding/Sources/AppleServerLoginClient.swift b/Projects/Feature/Onboarding/Sources/AppleServerLoginClient.swift new file mode 100644 index 0000000..505ee76 --- /dev/null +++ b/Projects/Feature/Onboarding/Sources/AppleServerLoginClient.swift @@ -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.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 + } + ) + } +} diff --git a/Projects/Feature/Onboarding/Sources/IntroFeature.swift b/Projects/Feature/Onboarding/Sources/IntroFeature.swift index 8ce301f..b30fb97 100644 --- a/Projects/Feature/Onboarding/Sources/IntroFeature.swift +++ b/Projects/Feature/Onboarding/Sources/IntroFeature.swift @@ -7,6 +7,7 @@ import Foundation import PlatformKakaoAuth +import PlatformAppleAuth import DoriNetwork import ComposableArchitecture @@ -26,6 +27,8 @@ public struct IntroFeature : Sendable { public enum Action: Equatable, Sendable { case kakaoLoginButtonTapped case kakaoLoginResponse(Result) + case appleLoginButtonTapped + case appleLoginResponse(Result) case errorAlertDismissed case delegate(Delegate) @@ -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 { switch action { @@ -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 @@ -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 + } +} diff --git a/Projects/Feature/Onboarding/Sources/IntroView.swift b/Projects/Feature/Onboarding/Sources/IntroView.swift index 379b64a..cd08d4f 100644 --- a/Projects/Feature/Onboarding/Sources/IntroView.swift +++ b/Projects/Feature/Onboarding/Sources/IntroView.swift @@ -74,9 +74,10 @@ public struct IntroView: View { Spacer() kakaoLoginButton + appleLoginButton } .alert( - "카카오 로그인 실패", + "로그인 실패", isPresented: Binding( get: { store.errorMessage != nil }, set: { isPresented in @@ -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) } } diff --git a/Projects/Feature/Onboarding/Tests/IntroFeatureTests.swift b/Projects/Feature/Onboarding/Tests/IntroFeatureTests.swift new file mode 100644 index 0000000..51f5af5 --- /dev/null +++ b/Projects/Feature/Onboarding/Tests/IntroFeatureTests.swift @@ -0,0 +1,149 @@ +// +// IntroFeatureTests.swift +// Dori-iOS +// + +import ComposableArchitecture +import Testing +import DoriNetwork +import PlatformAppleAuth +import PlatformKakaoAuth +@testable import FeatureOnboarding + +@Suite("IntroFeature") +struct IntroFeatureTests { + + // MARK: - Apple Login + + @Test("Apple 로그인 성공 → loginSucceeded=true, delegate(.loginSucceeded) 발송") + @MainActor + func appleLogin_success() async { + let store = TestStore(initialState: IntroFeature.State()) { + IntroFeature() + } withDependencies: { + $0.appleAuthClient.login = { + AppleAuthCredential( + identityToken: "test-identity-token", + firstName: "길동", + lastName: "홍", + email: "test@example.com" + ) + } + $0.appleServerLoginClient.login = { _, _ in + SocialLoginResponse(accessToken: "test-jwt", refreshToken: "test-refresh", id: 1) + } + } + + await store.send(.appleLoginButtonTapped) { + $0.isLoading = true + } + await store.receive(\.appleLoginResponse.success) { + $0.isLoading = false + $0.loginSucceeded = true + } + await store.receive(\.delegate.loginSucceeded) + } + + @Test("AppleAuthClient 실패 → errorMessage 설정") + @MainActor + func appleLogin_authClientFails() async { + let store = TestStore(initialState: IntroFeature.State()) { + IntroFeature() + } withDependencies: { + $0.appleAuthClient.login = { throw StubError() } + } + + await store.send(.appleLoginButtonTapped) { + $0.isLoading = true + } + await store.receive(\.appleLoginResponse.failure) { + $0.isLoading = false + $0.errorMessage = StubError.message + } + } + + @Test("AppleServerLoginClient 실패 → errorMessage 설정") + @MainActor + func appleLogin_serverFails() async { + let store = TestStore(initialState: IntroFeature.State()) { + IntroFeature() + } withDependencies: { + $0.appleAuthClient.login = { + AppleAuthCredential(identityToken: "test-identity-token") + } + $0.appleServerLoginClient.login = { _, _ in throw StubError() } + } + + await store.send(.appleLoginButtonTapped) { + $0.isLoading = true + } + await store.receive(\.appleLoginResponse.failure) { + $0.isLoading = false + $0.errorMessage = StubError.message + } + } + + @Test("에러 알림 해제 → errorMessage=nil") + @MainActor + func errorAlertDismissed_clearsMessage() async { + var initialState = IntroFeature.State() + initialState.errorMessage = "이전 에러" + let store = TestStore(initialState: initialState) { + IntroFeature() + } + + await store.send(.errorAlertDismissed) { + $0.errorMessage = nil + } + } + + // MARK: - Kakao Login + + @Test("Kakao 로그인 성공 → loginSucceeded=true, delegate(.loginSucceeded) 발송") + @MainActor + func kakaoLogin_success() async { + let store = TestStore(initialState: IntroFeature.State()) { + IntroFeature() + } withDependencies: { + $0.kakaoAuthClient.login = { "test-kakao-token" } + $0.kakaoServerLoginClient.login = { _ in + SocialLoginResponse(accessToken: "test-jwt", refreshToken: "test-refresh", id: 1) + } + } + + await store.send(.kakaoLoginButtonTapped) { + $0.isLoading = true + } + await store.receive(\.kakaoLoginResponse.success) { + $0.isLoading = false + $0.loginSucceeded = true + } + await store.receive(\.delegate.loginSucceeded) + } + + @Test("KakaoServerLoginClient 실패 → errorMessage 설정") + @MainActor + func kakaoLogin_serverFails() async { + let store = TestStore(initialState: IntroFeature.State()) { + IntroFeature() + } withDependencies: { + $0.kakaoAuthClient.login = { "test-kakao-token" } + $0.kakaoServerLoginClient.login = { _ in throw StubError() } + } + + await store.send(.kakaoLoginButtonTapped) { + $0.isLoading = true + } + await store.receive(\.kakaoLoginResponse.failure) { + $0.isLoading = false + $0.errorMessage = StubError.message + } + } +} + +// MARK: - Helpers + +private struct StubError: LocalizedError { + static let message = "테스트 에러" + var errorDescription: String? { Self.message } +} diff --git a/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroLoadingSnapshotTests/test_intro_loading_dark.1.png b/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroLoadingSnapshotTests/test_intro_loading_dark.1.png index be552b4..fbed13f 100644 Binary files a/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroLoadingSnapshotTests/test_intro_loading_dark.1.png and b/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroLoadingSnapshotTests/test_intro_loading_dark.1.png differ diff --git a/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroLoadingSnapshotTests/test_intro_loading_light.1.png b/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroLoadingSnapshotTests/test_intro_loading_light.1.png index db3b8e1..ced76f1 100644 Binary files a/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroLoadingSnapshotTests/test_intro_loading_light.1.png and b/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroLoadingSnapshotTests/test_intro_loading_light.1.png differ diff --git a/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroSnapshotTests/test_intro_dark.1.png b/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroSnapshotTests/test_intro_dark.1.png index be552b4..fbed13f 100644 Binary files a/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroSnapshotTests/test_intro_dark.1.png and b/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroSnapshotTests/test_intro_dark.1.png differ diff --git a/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroSnapshotTests/test_intro_light.1.png b/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroSnapshotTests/test_intro_light.1.png index db3b8e1..ced76f1 100644 Binary files a/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroSnapshotTests/test_intro_light.1.png and b/Projects/Feature/Onboarding/Tests/Snapshot/__Snapshots__/IntroSnapshotTests/test_intro_light.1.png differ diff --git a/Projects/Feature/Project.swift b/Projects/Feature/Project.swift index c7ea2d7..9a4b697 100644 --- a/Projects/Feature/Project.swift +++ b/Projects/Feature/Project.swift @@ -18,6 +18,7 @@ let project = Project.dori( DoriModules.core.module.projectDependency, DoriModules.network.module.projectDependency, DoriModules.kakaoAuth.module.projectDependency, + DoriModules.appleAuth.module.projectDependency, .external(.composableArchitecture) ] ), diff --git a/Projects/Infra/DoriNetwork/Sources/Auth/AuthEndpoints.swift b/Projects/Infra/DoriNetwork/Sources/Auth/AuthEndpoints.swift index d669a70..22a36a7 100644 --- a/Projects/Infra/DoriNetwork/Sources/Auth/AuthEndpoints.swift +++ b/Projects/Infra/DoriNetwork/Sources/Auth/AuthEndpoints.swift @@ -24,6 +24,52 @@ public struct KakaoLoginEndpoint: Endpoint { } } +public struct AppleLoginUserInfo: Equatable, Sendable { + public let firstName: String? + public let lastName: String? + public let email: String? + + public init( + firstName: String? = nil, + lastName: String? = nil, + email: String? = nil + ) { + self.firstName = firstName + self.lastName = lastName + self.email = email + } +} + +public struct AppleLoginEndpoint: Endpoint { + public let baseURL: String + public let path: String = "/auth/login/apple" + public let method: HTTPMethod = .POST + public let headers: [String: String] = [:] + public let queryParameters: [String: String] = [:] + public let body: Data? + + public init( + identityToken: String, + user: AppleLoginUserInfo? = nil, + baseURL: String = NetworkConfig.baseURL + ) { + self.baseURL = baseURL + + var params: [String: any Sendable] = ["identityToken": identityToken] + if let user { + var userDict: [String: any Sendable] = [:] + var nameDict: [String: any Sendable] = [:] + if let firstName = user.firstName { nameDict["firstName"] = firstName } + if let lastName = user.lastName { nameDict["lastName"] = lastName } + if !nameDict.isEmpty { userDict["name"] = nameDict } + if let email = user.email { userDict["email"] = email } + if !userDict.isEmpty { params["user"] = userDict } + } + let parameters = params as Parameters + self.body = try? JSONSerialization.data(withJSONObject: parameters) + } +} + // MARK: - Auth Endpoints (logout / withdraw / refresh) public struct LogoutEndpoint: Endpoint { diff --git a/Projects/Infra/DoriNetwork/Sources/Endpoint.swift b/Projects/Infra/DoriNetwork/Sources/Endpoint.swift index 06c802c..12e1e84 100644 --- a/Projects/Infra/DoriNetwork/Sources/Endpoint.swift +++ b/Projects/Infra/DoriNetwork/Sources/Endpoint.swift @@ -16,6 +16,9 @@ public enum HTTPMethod: String, Sendable { case PATCH } +// MARK: - Parameters +public typealias Parameters = [String: any Sendable] + // MARK: - Base Endpoint Protocol public protocol Endpoint: Sendable { var baseURL: String { get } diff --git a/Projects/Platform/AppleAuth/Sources/AppleAuthClient.swift b/Projects/Platform/AppleAuth/Sources/AppleAuthClient.swift new file mode 100644 index 0000000..d0ff244 --- /dev/null +++ b/Projects/Platform/AppleAuth/Sources/AppleAuthClient.swift @@ -0,0 +1,160 @@ +// +// AppleAuthClient.swift +// Dori-iOS +// +// Created by 강동영 on 6/25/26. +// + +import Foundation +import UIKit +import AuthenticationServices +import ComposableArchitecture + +public struct AppleAuthCredential: Equatable, Sendable { + public let identityToken: String + public let firstName: String? + public let lastName: String? + public let email: String? + + public init( + identityToken: String, + firstName: String? = nil, + lastName: String? = nil, + email: String? = nil + ) { + self.identityToken = identityToken + self.firstName = firstName + self.lastName = lastName + self.email = email + } +} + +public struct AppleAuthClient: Sendable { + public var login: @Sendable () async throws -> AppleAuthCredential + + public init(login: @escaping @Sendable () async throws -> AppleAuthCredential) { + self.login = login + } +} + +private enum AppleAuthClientError: LocalizedError { + case missingIdentityToken + case invalidIdentityTokenEncoding + + var errorDescription: String? { + switch self { + case .missingIdentityToken: + return "Apple 인증 토큰을 가져올 수 없습니다." + case .invalidIdentityTokenEncoding: + return "Apple 인증 토큰을 해석할 수 없습니다." + } + } +} + +extension AppleAuthClient: DependencyKey { + public static let liveValue = Self( + login: { + try await loginWithAppleID() + } + ) + + public static let testValue = Self( + login: { + AppleAuthCredential( + identityToken: "test-identity-token", + firstName: "길동", + lastName: "홍", + email: "test@example.com" + ) + } + ) + + @MainActor + private static func loginWithAppleID() async throws -> AppleAuthCredential { + try await withCheckedThrowingContinuation { continuation in + let provider = ASAuthorizationAppleIDProvider() + let request = provider.createRequest() + request.requestedScopes = [.fullName, .email] + + let controller = ASAuthorizationController(authorizationRequests: [request]) + let delegate = AppleAuthControllerDelegate(continuation: continuation) + controller.delegate = delegate + controller.presentationContextProvider = delegate + delegate.retain() + controller.performRequests() + } + } +} + +@MainActor +private final class AppleAuthControllerDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { + private var continuation: CheckedContinuation? + private var retainedSelf: AppleAuthControllerDelegate? + + init(continuation: CheckedContinuation) { + self.continuation = continuation + } + + func retain() { + retainedSelf = self + } + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { + defer { retainedSelf = nil } + + guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { + continuation?.resume(throwing: AppleAuthClientError.missingIdentityToken) + continuation = nil + return + } + + guard let tokenData = credential.identityToken else { + continuation?.resume(throwing: AppleAuthClientError.missingIdentityToken) + continuation = nil + return + } + + guard let identityToken = String(data: tokenData, encoding: .utf8) else { + continuation?.resume(throwing: AppleAuthClientError.invalidIdentityTokenEncoding) + continuation = nil + return + } + + let result = AppleAuthCredential( + identityToken: identityToken, + firstName: credential.fullName?.givenName, + lastName: credential.fullName?.familyName, + email: credential.email + ) + continuation?.resume(returning: result) + continuation = nil + } + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithError error: Error + ) { + defer { retainedSelf = nil } + continuation?.resume(throwing: error) + continuation = nil + } + + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + let keyWindow = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first { $0.isKeyWindow } + + return keyWindow ?? ASPresentationAnchor() + } +} + +public extension DependencyValues { + var appleAuthClient: AppleAuthClient { + get { self[AppleAuthClient.self] } + set { self[AppleAuthClient.self] = newValue } + } +} diff --git a/Projects/Platform/Project.swift b/Projects/Platform/Project.swift index 0d92c8e..bc7787d 100644 --- a/Projects/Platform/Project.swift +++ b/Projects/Platform/Project.swift @@ -20,6 +20,12 @@ let project = Project.dori( .external(.kakaoSDKUser) ] ), + .doriFramework( + DoriModules.appleAuth.module, + dependencies: [ + .external(.composableArchitecture) + ] + ), .doriFramework( DoriModules.keychain.module, dependencies: [ diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 2920595..1e07655 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -8,6 +8,12 @@ import ProjectDescription let packageSettings = PackageSettings( productTypes: [ "ComposableArchitecture": .framework, + "Dependencies": .framework, + "CombineSchedulers": .framework, + "Clocks": .framework, + "CasePaths": .framework, + "SwiftNavigation": .framework, + "ConcurrencyExtras": .framework, "Swinject": .framework, "Alamofire": .framework, "KakaoSDKCommon": .framework, @@ -17,7 +23,8 @@ let packageSettings = PackageSettings( baseSettings: .settings( base: [ "CODE_SIGNING_ALLOWED": "NO", - "CODE_SIGN_IDENTITY": "" + "CODE_SIGN_IDENTITY": "", + "SWIFT_ENABLE_EXPLICIT_MODULES": "NO" ] ) ) @@ -27,6 +34,10 @@ let package = Package( name: "DoriDependencies", dependencies: [ .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.25.5"), + // swift-navigation 2.10.1+ / swift-case-paths 1.8.0+ have Xcode 26.3 macro host build issues. + // Pin to versions known to work (same as develop CI June 15). + .package(url: "https://github.com/pointfreeco/swift-navigation", .upToNextMinor(from: "2.8.0")), + .package(url: "https://github.com/pointfreeco/swift-case-paths", .upToNextMinor(from: "1.7.3")), .package(url: "https://github.com/Swinject/Swinject.git", from: "2.9.1"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.11.1"), .package(url: "https://github.com/kakao/kakao-ios-sdk", from: "2.0.0"), diff --git a/Tuist/ProjectDescriptionHelpers/DoriTargets.swift b/Tuist/ProjectDescriptionHelpers/DoriTargets.swift index 6b7480d..06ba03e 100644 --- a/Tuist/ProjectDescriptionHelpers/DoriTargets.swift +++ b/Tuist/ProjectDescriptionHelpers/DoriTargets.swift @@ -63,6 +63,7 @@ public enum DoriModules: CaseIterable, Sendable { case network case networkImpl case kakaoAuth + case appleAuth case keychain case fcm case onboarding @@ -85,6 +86,8 @@ public enum DoriModules: CaseIterable, Sendable { DoriModule(name: "DoriNetworkImpl", layer: .infra) case .kakaoAuth: DoriModule(name: "PlatformKakaoAuth", layer: .platform, directoryName: "KakaoAuth") + case .appleAuth: + DoriModule(name: "PlatformAppleAuth", layer: .platform, directoryName: "AppleAuth") case .keychain: DoriModule(name: "PlatformKeychain", layer: .platform, directoryName: "Keychain") case .fcm: