signInWithApple
This commit is contained in:
10
LabWise/LabWise.entitlements
Normal file
10
LabWise/LabWise.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ final class ProfileViewModel {
|
||||
var isDeleting = false
|
||||
|
||||
// Editable fields
|
||||
var displayName = ""
|
||||
var piFirstName = ""
|
||||
var bldgCode = ""
|
||||
var lab = ""
|
||||
@@ -36,6 +37,13 @@ final class ProfileViewModel {
|
||||
isSaving = true
|
||||
defer { isSaving = false }
|
||||
do {
|
||||
let trimmedName = displayName.trimmingCharacters(in: .whitespaces)
|
||||
if !trimmedName.isEmpty && trimmedName != AppState.shared.currentUser?.name {
|
||||
let updatedUser = try await authClient.updateName(trimmedName)
|
||||
await MainActor.run {
|
||||
AppState.shared.currentUser = updatedUser
|
||||
}
|
||||
}
|
||||
let body = UserProfileUpsertBody(
|
||||
piFirstName: piFirstName,
|
||||
bldgCode: bldgCode,
|
||||
@@ -88,6 +96,11 @@ struct ProfileView: View {
|
||||
Form {
|
||||
if let user = appState.currentUser {
|
||||
Section("Account") {
|
||||
if viewModel.isEditing {
|
||||
TextField("Name", text: $viewModel.displayName)
|
||||
} else {
|
||||
LabeledContent("Name", value: user.name ?? "—")
|
||||
}
|
||||
LabeledContent("Email", value: user.email)
|
||||
}
|
||||
}
|
||||
@@ -149,6 +162,7 @@ struct ProfileView: View {
|
||||
.disabled(viewModel.isSaving)
|
||||
} else {
|
||||
Button("Edit") {
|
||||
viewModel.displayName = appState.currentUser?.name ?? ""
|
||||
viewModel.isEditing = true
|
||||
}
|
||||
}
|
||||
@@ -157,6 +171,7 @@ struct ProfileView: View {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button("Cancel") {
|
||||
viewModel.isEditing = false
|
||||
viewModel.displayName = appState.currentUser?.name ?? ""
|
||||
if let p = viewModel.profile {
|
||||
viewModel.piFirstName = p.piFirstName
|
||||
viewModel.bldgCode = p.bldgCode
|
||||
|
||||
Reference in New Issue
Block a user