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: