signInWithApple

This commit is contained in:
2026-04-10 01:44:02 -05:00
parent ccaf0e179f
commit a8747a4df0
5 changed files with 250 additions and 3 deletions

View File

@@ -9,8 +9,14 @@ struct LoginView: View {
@State private var password = ""
@State private var isLoading = false
@State private var isGoogleLoading = false
@State private var isAppleLoading = false
@State private var errorMessage: String?
// Apple sign-in: we generate a fresh raw nonce each time the button is
// tapped, hash it, hand the hash to Apple, then forward the same hash
// to Better Auth so it can verify the JWT echoed back in `nonce`.
@State private var currentAppleRawNonce: String?
private let authClient = AuthClient()
var body: some View {
@@ -86,8 +92,8 @@ struct LoginView: View {
.background(Color(.brandPrimary))
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
.disabled(email.isEmpty || password.isEmpty || isLoading || isGoogleLoading)
.opacity((email.isEmpty || password.isEmpty || isLoading || isGoogleLoading) ? 0.5 : 1)
.disabled(email.isEmpty || password.isEmpty || isLoading || isGoogleLoading || isAppleLoading)
.opacity((email.isEmpty || password.isEmpty || isLoading || isGoogleLoading || isAppleLoading) ? 0.5 : 1)
HStack {
Rectangle()
@@ -104,7 +110,24 @@ struct LoginView: View {
GoogleSignInButton(isLoading: isGoogleLoading) {
Task { await signInWithGoogle() }
}
.disabled(isLoading || isGoogleLoading)
.disabled(isLoading || isGoogleLoading || isAppleLoading)
SignInWithAppleButton(
onRequest: { request in
let raw = AuthClient.generateAppleNonceRaw()
currentAppleRawNonce = raw
request.requestedScopes = [.fullName, .email]
request.nonce = AuthClient.sha256Hex(raw)
},
onCompletion: { result in
Task { await handleAppleCompletion(result) }
}
)
.signInWithAppleButtonStyle(.black)
.frame(height: 44)
.clipShape(RoundedRectangle(cornerRadius: 10))
.disabled(isLoading || isGoogleLoading || isAppleLoading)
.opacity(isAppleLoading ? 0.5 : 1)
}
.padding(24)
.background(Color(UIColor.secondarySystemGroupedBackground))
@@ -143,6 +166,58 @@ struct LoginView: View {
}
}
@MainActor
private func handleAppleCompletion(_ result: Result<ASAuthorization, Error>) async {
isAppleLoading = true
errorMessage = nil
defer {
isAppleLoading = false
currentAppleRawNonce = nil
}
switch result {
case .failure(let error):
if let asError = error as? ASAuthorizationError, asError.code == .canceled {
return // user cancelled silent
}
errorMessage = "Apple sign-in failed. Please try again."
case .success(let authorization):
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
let identityTokenData = credential.identityToken,
let identityToken = String(data: identityTokenData, encoding: .utf8) else {
errorMessage = "Apple sign-in failed: missing identity token."
return
}
// The hashed nonce we sent to Apple Better Auth needs the same
// value to compare against the JWT's `nonce` claim.
let hashedNonce = currentAppleRawNonce.map(AuthClient.sha256Hex)
// Apple only delivers fullName / email the very first time the
// user authorises this app. Forward whatever we got Better
// Auth's getUserInfo will use these to populate the user record.
let firstName = credential.fullName?.givenName
let lastName = credential.fullName?.familyName
let email = credential.email
do {
let user = try await authClient.signInWithApple(
idToken: identityToken,
hashedNonce: hashedNonce,
firstName: firstName,
lastName: lastName,
email: email
)
appState.signedIn(user: user)
} catch APIError.httpError(_, let data) {
let msg = String(data: data, encoding: .utf8) ?? "Apple sign-in failed."
errorMessage = msg
} catch {
errorMessage = "Apple sign-in failed. Please check your connection."
}
}
}
@MainActor
private func signInWithGoogle() async {
guard let window = UIApplication.shared.connectedScenes