Sign in with Apple Authentication Flow
Apps generated with AuthKit include a complete authentication flow for signing in with Apple. This flow includes:
- Signing In - The user can sign in with their email and password.
- Signing Out - The user can sign out of their account.
- Account Creation - Automatic account creation during the sign in process
- Account Deletion - The user can delete their account
Signing In & Signing Out, Account Creation
If the user is not signed in and tries to access features that require authentication, they will be presented with a sign in sheet, that will prompt them to sign in. After pressing Sign in with Apple, a native Apple sign in sheet will be shown, where the user can sign in with their Apple Account.
Demo
Walkthrough
In the demo, the user is not signed in and tries to open the account settings, which requires the user to be signed in (Lock SwiftUI Views to Signed-In Users).
When presented with the SignInView
, the user has the option to sign in with their email and password, reset their password, create a new account, or sign in with Apple.
In this case, the user signs in with Apple, and is then able to access the account settings. After pressing on the SignInWithAppleButton
,
the handleSignInWithAppleRequest
function of DB
is called, which handle the initial sign in request and generate a new nonce string. After the initial request is done,
the onComplete function calls the handleSignInWithAppleCompletion
function of DB
, which will handle the sign in process with Firebase.
This will either create a new user or sign in an existing user, and the user will be redirected to the account settings. In contrast to the account creation process with email and password, the default account username automatically gets inferred by sign in with Apple, instead of being randomly generated.
If there was an error during the sign-in process, it is handled by showing the user a corresponding in-app notification. If the sign in was successful, the sign in sheet will be hidden.
The sign in state is automatically saved in the DB
object and can be access through the authState
variable.
// ...
public enum AuthState {
case signedOut
case signedInUnverified // means user is signed in but hasn't verified his email address yet
case signedIn
}
// ...
@MainActor
public class DB: ObservableObject {
// ...
@Published public var authState: AuthState = .signedOut
// ...
}
// ...
SignInView
// ...
public struct SignInView: View {
@ObservedObject var db: DB
// ...
public var body: some View {
NavigationStack(path: $signInFlowPath) {
// ...
VStack {
// ... sign in hero section
// ... sign in fields
SignUpButtons(shouldShowEmailSignUpScreen: {
signInFlowPath.append(SignInFlowPath.emailSignUp)
})
}
// ...
}
// ...
}
}
SignUpButtons
Contains the buttons related to social sign ins and account creation.
struct SignUpButtons: View {
@EnvironmentObject var db: DB
var body: some View {
VStack(alignment: .leading) {
AppleAuthButton()
// ... sign up with email button
// ... by signing in you agree to tos, privacy policy text
}
}
}
struct AppleAuthButton: View {
@EnvironmentObject var db: DB
// ...
var body: some View {
SignInWithAppleButton(
.continue,
onRequest: { request in
// ...
db.handleSignInWithAppleRequest(request)
},
onCompletion: { complete in
// ...
try await db.handleSignInWithAppleCompletion(complete) { /* ... handle successful sign in */ }
// ...
}
)
// ...
}
}
Apple Sign in Handling in DB
object
@MainActor
public class DB: ObservableObject {
public func handleSignInWithAppleRequest(_ request: ASAuthorizationAppleIDRequest) {
request.requestedScopes = [.fullName, .email]
// ... nonce generation
}
public func handleSignInWithAppleCompletion(_ result: Result<ASAuthorization, Error>) async throws -> Bool {
// ...
if case .success(let authResults) = result {
if let appleIDCredential = authResults.credential as? ASAuthorizationAppleIDCredential {
// ...
let credential = OAuthProvider.credential(/* ... */)
do {
let result = try await Auth.auth().signIn(with: credential)
await updateDisplayName(for: result.user, with: appleIDCredential)
// ...
} catch {
// ... error handling
}
}
// ...
}
// ...
}
}
Account Deletion
Account deletion can be initiated in the the account settings (AccountSettingsView
). The user can press
the "Delete Account" button, which requires an additional confirmation to prevent accidental deletion.
After that, a reauthentication sheet is shown, prompting the user to sign in with apple to confirm the identity.
On successful reauthentication, the user is signed out and their account is deleted.
Demo
AccountSettingsView
public struct AccountSettingsView: View {
// ...
@EnvironmentObject var db: DB
@State private var showAccountDeleteDialog = false
@State private var reAuthSheetRef = false
// ...
public var body: some View {
List {
// ... account settings header (image, name, email)
Section {
// ... change name button
// ... photos picker
// ... change password button
Button("Delete Account", role: .destructive) {
// ...
showAccountDeleteDialog = true
}
.confirmationDialog(
"Are you sure you want to delete your Account?", isPresented: $showAccountDeleteDialog
) {
Button("Confirm Account Deletion", role: .destructive) {
// show sheet to re-authenticate
showReAuthSheet(db: db, reAuthSheetRef: $reAuthSheetRef) { result in
switch result {
case .success: // on success, hide re-auth sheet and show password sheet after delay
// ...
await Task.sleep(for: .seconds(0.5))
await db.deleteUser()
// ...
dismissReAuthSheet(reAuthSheetRef: $reAuthSheetRef)
case .canceled: // on cancel, hide re-auth sheet
dismissReAuthSheet(reAuthSheetRef: $reAuthSheetRef)
case .forgotPassword:
// ... show password reset sheet
}
}
}
}
// ... sign out button
}
}
// ...
}
// ...
}
The deleteUser()
function of the DB
object
This function deletes the current user's account and logs the user out. This function also requires additional token revocation steps (opens in a new tab) in comparison to deleting users that signed in with email and password. It is an asynchronous function that throws an error if the deletion fails.
extension DB {
public func deleteUser() async throws {
// ...
if provider == .apple {
// ...
do {
// ...
try await Auth.auth().revokeToken(withAuthorizationCode: authCodeString)
// ...
} catch {
// ... error handling
}
}
// ...
do {
// ...
try await currentUser.delete()
// ...
try signOut()
// ...
} catch {
// ... error handling
}
}
}