📦 SwiftyLaunch Modules
🔐 AuthKit
Email Sign-In Flow

Sign in with Email & Password Authentication Flow

AuthKit (SwiftyLaunch Module) - Sign in with Email & Password Authentication Flow

Apps generated with AuthKit include a complete authentication flow for signing in with email and password. 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 - An account creation screen with password strength indicator
  • Email Verification After account creation, the user will receive an email to verify their email address
  • Password Reset / Forgot Password - The user can reset their password pressing on the forgot password button, after which they will receive an email to reset their password
  • Account Deletion - The user can delete their account

Signing In & Signing Out

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 entering the email and password, and pressing the sign in button, the user will be signed in (if their credentials are correct).

Demo

Sign In and Out 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 their email and password, and is then able to access the account settings. After the user enters their email and password, the app calls the signIn() function of the shared DB environment object, which signs the user in. If there was an error, 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.

FirebaseBackend.swift
// ...
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

SignInView.swift
// ...
public struct SignInView: View {
	@ObservedObject var db: DB
    // ...
	public var body: some View {
		NavigationStack(path: $signInFlowPath) {
            // ...
            VStack {
                // ... sign in hero section
                EmailInputFields(
                    forgotPasswordAction: { /* ... */ },
                    continueAction: { email, password in
                        // ...
                        try await db.signIn(email: email, password: password)
                        // ...
                    })
                // ... sign up button + sign in with apple button
            }
            // ...
		}
        // ...
	}
}

The signIn() function of the DB object

This function takes two parameters: the email and password of the user. It is an asynchronous function that throws an error if the sign in fails.

FirebaseBackend.swift
extension DB {
	public func signIn(email: String, password: String) async throws { }
}

Account Creation & Email Verification

To create an account using email and password, the user has to press Sign Up using Email on the sign in sheet. This will redirect the user to the SignUpView, where they can enter their email and password. The password field includes a password strength indicator below. After entering their email and password, the user can press the sign up button to create an account. This calls the signUp() function of the DB object. This function creates a new user with a randomly generated username, and sends a verification email to the user. The user will then be redirected to the VerifyEmailView.

The sign in process in not complete yet. Every time the user re-opens the app, and the sign-in view is shown, it automatically redirects the user to the VerifyEmailView if the user hasn't verified their email address yet. Because the user will probably minimize the app to verify their email, the app will check the verification state every time the app will come to the foreground.

💡

To prevent abuse, we require the user to have their email verified. Meaning, that even if the user tries to log in using another device with the same credentials as before, they will be "logged in" and straight away redirected to the VerifyEmailView. The authentication state in authState will be set to .signedInUnverified and should be treated as if they are not signed in.

Demo

SignInView

SignInView.swift
// ...
public struct SignInView: View {
 
    // ...
	@ObservedObject var db: DB
	@State var signInFlowPath: NavigationPath
    // ...
 
	public init(/* ... */) {
        // ...
		var navPath = NavigationPath()
        // if the user is signed in, but hasn't verified their email
        // address yet, redirect them to the verify email view
		if let user = db.currentUser {
			if db.currentUserProvider == .email && !user.isEmailVerified {
				navPath.append(SignInFlowPath.confirmEmail)
			} else {
				// ... user signed in, hide the sign in sheet
			}
		}
		self.signInFlowPath = navPath
	}
 
	public var body: some View {
		NavigationStack(path: $signInFlowPath) {
			ScrollView {
				VStack {
					// ... sign in hero section
 
					// ... sign in fields
 
                    // ...
					SignUpButtons(shouldShowEmailSignUpScreen: {
						signInFlowPath.append(SignInFlowPath.emailSignUp)
					})
                    // ...
				}
                // ...
				.navigationDestination(for: SignInFlowPath.self) { authScreen in
					switch authScreen {
						case .emailSignUp:
							SignUpView()
								.environmentObject(db)
						case .confirmEmail:
							VerifyEmailView()
								.environmentObject(db)
								.navigationBarBackButtonHidden(true)
						case .forgotPassword:
                        // ...
					}
				}
				// ...
			}
		}
	}
}

SignUpView

SignUpView.swift
struct SignUpView: View {
 
	@EnvironmentObject var db: DB
 
	var body: some View {
		VStack {
			// ... hero view
			EmailInputFields { email, password in
                // ...
				try await db.signUp(email: email, password: password)
                // ...
			}
		}
        // ...
	}
}

VerifyEmailView

VerifyEmailView.swift
struct VerifyEmailView: View {
 
	@Environment(\.scenePhase) var scenePhase
	@EnvironmentObject var db: DB
	@StateObject var verificationTimer = VerificationTimer()
 
	public var body: some View {
		if let user = db.currentUser, let email = user.email {
			VStack {
 
				// ... hero view
 
                // allow the user to resend the verification email when the timer runs out
				Button(
					"Resend Verification Email\(verificationTimer.secondsLeft > 0 ? " (\(verificationTimer.secondsLeft))" : "")"
				) {
					verificationTimer.startCountdown()
                    // ...
                    try await db.sendVerificationEmail()
                    //...
				}
                .disabled(verificationTimer.secondsLeft > 0)
 
                // ...
 
				// ... sign out button
			}
            // ...
 
            // app in foreground -> refresh user data to check verification state
			.onChange(of: scenePhase) {
				if scenePhase == .active {
					Task {
						await db.refreshUserData()
					}
				}
			}
 
            // ...
		} else {
            // ... invalid state
		}
	}
}

The signUp() function of the DB object

This function takes two parameters: the email and password of the user. It is an asynchronous function that throws an error if the sign in fails. Will create a new user in the Firebase Authentication service, give a new new to the user and send a verification email.

FirebaseBackend.swift
extension DB {
	public func signUp(email: String, password: String) async throws {
        // ...
		do {
			let result = try await Auth.auth().createUser(withEmail: email, password: password)
            // ...
			try await newDisplayName(randomNicknameGenerator()) // will update the current user's display name
			do {
				try await sendVerificationEmail()
			} catch {
                // ... handle error
			}
            // ...
		} catch {
			// ... handle error
		}
	}
}

Password Reset / Forgot Password

If the user forgets their password, but remembers their email, they have the option to press the "Forgot Password" icon on the sign in sheet. This will redirect them to the ForgotPasswordView, where they can enter their email. After entering their email address, they will receive an email with a link to reset their password. This opens webpage served by Firebase, where the user can enter a new password. When the users opens the app, they have the option to go back and try to sign in again.

Demo

SignInView

SignInView.swift
public struct SignInView: View {
 
	@ObservedObject var db: DB
 
    // ...
 
	public var body: some View {
		NavigationStack(path: $signInFlowPath) {
			ScrollView {
				VStack {
 
                    // ... sign in hero section
 
					EmailInputFields(
						forgotPasswordAction: {
							signInFlowPath.append(SignInFlowPath.forgotPassword)
						},
						continueAction: { email, password in
							// ...
						})
 
					// ... sign up button + sign in with apple button
				}
                // ...
				.navigationDestination(for: SignInFlowPath.self) { authScreen in
					switch authScreen {
						case .emailSignUp:
							// ...
						case .confirmEmail:
                            // ...
						case .forgotPassword:
							ForgotPasswordView {
								signInFlowPath.removeLast(signInFlowPath.count) // on cancel, go back to sign in view
							}
							.environmentObject(db)
							.navigationBarBackButtonHidden(true)
					}
				}
				// ...
			}
		}
        // ...
	}
}

ForgotPasswordView

ForgotPasswordView.swift
struct ForgotPasswordView: View {
 
	@EnvironmentObject var db: DB
    // ...
	@State var email: String
    // ...
	@State private var emailSent = false
    // ...
 
	public var body: some View {
 
		VStack {
			// ... hero view
 
			TextField("Your Account Email", text: $email)
			// ...
 
			Button(emailSent ? "Request Sent" : "Send Request") {
				// ... validate email
 
				Task {
					emailSent = true
                    // ...
                    try await db.requestPasswordReset(email: email)
                    // ...
				}
			}
			// ...
			.disabled(emailSent)
            // ...
		}
        // ...
	}
}

The requestPasswordReset() function of the DB object

Sends a password reset email to the passed email address.

FirebaseBackend.swift
extension DB {
	public func requestPasswordReset(email: String) async throws {
        // ...
		do {
            // ...
			try await Auth.auth().sendPasswordReset(withEmail: email)
            // ...
		} catch {
			// ... handle error
		}
	}
}

Account Deletion

In contrast to other actions, account deletion doesn't happen on the SignInView, but rather in the account settings (AccountSettingsView). The user can press the "Delete Account" button in the account settings, which requires an additional confirmation to prevent accidental deletion. After that, a reauthentication sheet is shown, prompting the user to enter their current password to confirm the identity. On successful reauthentication, the user is signed out and their account is deleted.

Demo

Account Deletion Demo

AccountSettingsView

AccountSettingsView.swift
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. It is an asynchronous function that throws an error if the deletion fails.

FirebaseBackend.swift
extension DB {
    public func deleteUser() async throws {
        // ...
		do {
            // ...
			try await currentUser.delete()
            // ...
			try signOut()
            // ...
		} catch {
			// ... error handling
		}
	}
}