87 lines
3.9 KiB
Swift
87 lines
3.9 KiB
Swift
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<ASAuthorization, Error>) {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|