feat: add iOS support and harden receipt scanning
This commit is contained in:
86
app/ios/ScanReceipts/Features/Auth/AppleSignInButton.swift
Normal file
86
app/ios/ScanReceipts/Features/Auth/AppleSignInButton.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user