This guide explains how email verification works in Chef Mate after the recent updates to handle Supabase's email confirmation flow.
When a user signs up with Supabase, by default:
- Supabase creates a session immediately, even if email confirmation is required
- The app sees this session and considers the user authenticated
- The user needs to verify their email, but they're already "logged in"
The app now properly handles email verification by:
- Checking email confirmation status after sign-up
- Signing out unverified users immediately after registration
- Notifying the UI that email verification is required
- Requiring the user to verify their email before they can sign in
- Added new
AwaitingEmailVerificationstate containing the user's email - This state is observable by any part of the app
- Added
SignUpResultsealed class with two states:Success: User is authenticated (no email verification required)AwaitingEmailVerification: User needs to verify their email
- After sign-up, checks if
emailConfirmedAtis null - If null, signs out the user, sets state to
AwaitingEmailVerification, and returns the result - If confirmed, returns
Success
- Added
EmailVerificationRequiredoutput with the user's email - Handles the sign-up result and sends appropriate output to navigation
SettingsViewModelobserves the auth state and tracksemailAwaitingVerificationSettingsBlocImplmaps the email to aTextDatamessage usingcreateEmailVerificationMessage()- Message is exposed through the
verificationMessagefield in the model
- Added
EmailVerificationMessagecomposable to display a colored banner - Shows the message above sign-in/sign-up buttons when in unverified state
- Added helper function
createEmailVerificationMessage()to create the localized message - New string resource:
email_verification_required
- Handles the
EmailVerificationRequiredoutput by popping back to Settings - Settings screen automatically displays the verification message via state observation
- User enters email and password in sign-up form
- App calls Supabase sign-up
- Supabase creates account and sends verification email
- App checks if email is confirmed
- If not confirmed:
- App signs out the user immediately
- Sets auth state to
AwaitingEmailVerificationwith the user's email - Returns to Settings screen which shows verification message
- User receives email with verification link
- User clicks verification link in email
- User returns to app and signs in with their credentials
- Sign-in succeeds because email is now verified and message disappears
Create a dedicated screen that:
- Shows a message like "Please check your email to verify your account"
- Displays the email address used for registration
- Has a "Resend verification email" button
- Has a "Go back to sign in" button
Implementation:
// Add to Configuration
@Serializable
data class EmailVerification(val email: String) : Configuration()
// Update handleAuthenticationOutput
is AuthenticationBloc.Output.EmailVerificationRequired -> {
navigation.replaceCurrent(
Configuration.EmailVerification(output.email)
)
}If you want the user to be automatically signed in after clicking the verification link:
1. Configure Supabase Auth with redirect URL:
// In SupabaseModule.kt
install(Auth) {
scheme = "chefmate" // Your app's deep link scheme
host = "auth"
}2. Update sign-up to include redirect URL:
override suspend fun signUpWithEmailAndPassword(
email: String,
password: String,
): Result<SignUpResult> =
try {
supabaseClient.auth.signUpWith(Email) {
this.email = email
this.password = password
redirectUrl = "chefmate://auth/callback" // Deep link
}
val currentUser = supabaseClient.auth.currentUserOrNull()
val userNeedsConfirmation = currentUser?.emailConfirmedAt == null
if (userNeedsConfirmation) {
supabaseClient.auth.signOut()
Result.success(SignUpResult.AwaitingEmailVerification)
} else {
Result.success(SignUpResult.Success)
}
} catch (e: Exception) {
Result.failure(e)
}3. Handle deep links in your Android app:
Add to AndroidManifest.xml:
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="chefmate"
android:host="auth"
android:pathPrefix="/callback" />
</intent-filter>4. Handle deep link in MainActivity:
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.data?.let { uri ->
// Supabase will automatically handle the session
lifecycleScope.launch {
supabaseClient.auth.handleDeeplinks(uri)
}
}
}If you want to disable email verification for development:
- Go to Supabase Dashboard
- Navigate to Authentication → Settings
- Find "Enable email confirmations"
- Toggle it OFF
Set up Supabase to auto-confirm emails for specific domains (like test emails):
- Go to Supabase Dashboard
- Navigate to Authentication → Settings
- Add your test domain to "Auto-confirm email domains"
- Sign up with a new email
- Check your email for verification link
- App should return to sign-in screen
- Try to sign in (should fail with unverified email error)
- Click verification link in email
- Try to sign in again (should succeed)
You can customize the verification email in Supabase Dashboard:
- Go to Authentication → Email Templates
- Edit the "Confirm signup" template
- Customize the message and styling
If using deep linking, add your redirect URLs to the allowlist:
- Go to Authentication → URL Configuration
- Add your redirect URLs (e.g.,
chefmate://auth/callback)
- Decide on user experience: Do you want a verification screen or just return to sign-in?
- Implement deep linking (optional): If you want seamless verification via email link
- Add "resend verification email" feature: Allow users to request a new verification email
- Update error messages: Make it clear when sign-in fails due to unverified email
This implementation follows clean architecture principles:
- Separation of Concerns: Auth state is managed in the data layer, UI reacts to it
- Reusability: Any screen can observe the auth state and show appropriate messaging
- Testability: Each layer can be tested independently
- Single Source of Truth: Auth state is the single source for verification status
- Reactive: UI automatically updates when auth state changes
client/auth/data/public/src/commonMain/kotlin/com/plusmobileapps/chefmate/auth/data/AuthState.ktclient/auth/data/public/src/commonMain/kotlin/com/plusmobileapps/chefmate/auth/data/AuthenticationRepository.ktclient/auth/data/impl/src/commonMain/kotlin/com/plusmobileapps/chefmate/auth/data/impl/SupabaseAuthenticationRepository.ktclient/auth/ui/impl/src/commonMain/kotlin/com/plusmobileapps/chefmate/auth/ui/impl/AuthenticationViewModel.ktclient/auth/ui/public/src/commonMain/kotlin/com/plusmobileapps/chefmate/auth/ui/AuthenticationBloc.kt
client/settings/impl/src/commonMain/kotlin/com/plusmobileapps/chefmate/settings/impl/SettingsViewModel.ktclient/settings/impl/src/commonMain/kotlin/com/plusmobileapps/chefmate/settings/impl/SettingsBlocImpl.ktclient/settings/public/src/commonMain/kotlin/com/plusmobileapps/chefmate/settings/SettingsBloc.ktclient/settings/public/src/commonMain/kotlin/com/plusmobileapps/chefmate/settings/SettingsScreen.ktclient/settings/public/src/commonMain/kotlin/com/plusmobileapps/chefmate/settings/SettingsTextData.ktclient/settings/public/src/commonMain/composeResources/values/strings.xml
client/root/impl/src/commonMain/kotlin/com/plusmobileapps/chefmate/root/RootBlocImpl.kt