import SwiftUI import AuthenticationServices import Foundation // PersonNameComponentsFormatter /// "Mit Apple anmelden" — placed below the email/password form on /// `LoginView`. Wired to the real backend: `POST /api/auth/apple` /// (`src/app/api/auth/apple/route.ts`) verifies the identity token Apple /// hands back and returns a session exactly like `/api/auth/login` does — /// see `AuthAPI.appleSignIn` / `AppState.signInWithApple`. struct AppleSignInButton: View { @Environment(\.colorScheme) private var colorScheme @EnvironmentObject private var appState: AppState @State private var isSigningIn = false /// Local, Apple-specific failures (token decode, user cancelled) — kept /// separate from `appState.lastError` so a cancelled Apple sheet doesn't /// show a stale email/password error underneath it, or vice versa. @State private var localErrorMessage: String? var body: some View { VStack(spacing: ZenithSpacing.unit * 2) { SignInWithAppleButton(.signIn) { request in request.requestedScopes = [.fullName, .email] } onCompletion: { result in handle(result) } // Apple's Human Interface Guidelines fix this button's own shape // and colors — not overridable to match Zenith Silver's 0px // corners, and Apple explicitly does not allow restyling it. .signInWithAppleButtonStyle(colorScheme == .dark ? .white : .black) .frame(maxWidth: .infinity, minHeight: 50) .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) .disabled(isSigningIn) .overlay { if isSigningIn { ProgressView().tint(.white) } } // A backend-side failure (invalid token, server error, ...) sets // `appState.lastError`, which `LoginView` already renders above // this button — showing it a second time here would just // duplicate the same message. Only a LOCAL, pre-network failure // (decode error, user cancelled) is shown here. if let localErrorMessage { Text(localErrorMessage).zenithBodySmStyle(color: .zenithError) } } } private func handle(_ result: Result) { switch result { case .success(let authorization): guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential, let tokenData = credential.identityToken, let identityToken = String(data: tokenData, encoding: .utf8) else { localErrorMessage = "Apple-Anmeldung fehlgeschlagen. Bitte erneut versuchen." return } localErrorMessage = nil // Only present on the account's very first authorization with // this app — nil on every later sign-in, which is expected and // fine (AuthAPI.appleSignIn/the backend both treat it as optional). let fullName = credential.fullName.flatMap { components -> String? in let formatted = PersonNameComponentsFormatter.localizedString(from: components, style: .default) return formatted.isEmpty ? nil : formatted } isSigningIn = true Task { await appState.signInWithApple(identityToken: identityToken, fullName: fullName) isSigningIn = false } case .failure(let error): // The user cancelling the Apple Sign-In sheet also lands here // (ASAuthorizationError.canceled) — that's expected, not a // failure worth surfacing as an error. if (error as? ASAuthorizationError)?.code == .canceled { localErrorMessage = nil } else { localErrorMessage = error.localizedDescription } } } }