layou_auth 1.0.3
layou_auth: ^1.0.3 copied to clipboard
Firebase Auth package with Google, Apple, and Email sign-in/linking support. Provides ready-to-use widgets and Riverpod providers.
π LayouAuth #
Stop blocking users. Start building trust.
A Flutter authentication module that lets users experience your app first, authenticate later.
Why LayouAuth? β’ Quick Start β’ Screenshots β’ Examples β’ Docs
π― Why LayouAuth? #
This README is for everyone - designers, product managers, and developers. We talk UX strategy first, implementation second.
The Problem: 40% Drop-off at Login #
For years, apps forced users to "Sign up" before even seeing the first screen.
The truth: people want to TRY, not fill forms.
Every authentication step between download and your app's "aha moment" is a leak in your funnel.
The LayouAuth Philosophy #
This isn't just a package. It's a UX mindset + technical implementation.
For designers & product people: LayouAuth removes the biggest friction point in mobile apps. It's built on the belief that users should experience your app's value before you ask for their trust.
For developers: Stop writing Firebase Auth boilerplate. Get a production-ready auth system with beautiful UI in minutes. No boilerplate. No complexity. Just authentication that works.
For everyone: If your app requires a sign-up wall before showing any value, ask yourself: "Am I protecting my users, or protecting myself from admitting my onboarding isn't compelling enough?"
Anonymous-first auth forces you to build something worth coming back to.
π Real-world UX Strategies #
Here's what teams are testing in production:
Anonymous-first (recommended by layou_auth)
The approach:
- Users start anonymous by default
- Multiple CTAs throughout the app to link their account
- Prompt linking after meaningful engagement (post-paywall, after first save, etc.)
The results:
- Zero friction at launch β higher conversion
- Users experience the "aha moment" before committing
- Natural filter: if users don't want to link, the app didn't deliver value
Pro tip: For paid users, require account linking post-purchase. Anonymous + payment works for the initial conversion, but linking ensures they can restore purchases and access across devices.
π§ Email-only
The reality: It can feel restrictive, but if your app truly delivers value, retention stays strong. Some older apps still use this successfully.
π Apple/Google only
The experiment: Radical but clean. Removes choice paralysis, but be aware:
- Users might use "Hide My Email" with Apple Sign-In
- You'll get private relay emails like
abc123@privaterelay.appleid.com - It works, but plan for anonymous email addresses
The pattern that emerges: The best apps let users reach value instantly, then earn the right to ask for commitment.
β¨ What You Get #
β Without LayouAuth #
|
β With LayouAuth #
That's it. Sign-in, linking, errors, loading states, UIβall handled. |
Features for Product & UX Teams #
- π― Instant engagement - Users reach your app's value in seconds, not after a form
- π Built-in conversion funnel - Anonymous β engaged β linked = clear value signal
- π§ͺ A/B test ready - Test different linking prompts and timings
- π‘ Progressive trust building - Earn the right to ask for commitment
Features for Developers #
- π Ready-to-use UI - Beautiful bottom sheets, inline sections, and individual buttons. Dark mode included.
- π Account Linking - Convert anonymous users to permanent accounts seamlessly
- π§© Riverpod Integration - Providers for auth state, user info, and actions. No setup needed.
- π¨ Fully Customizable - Override strings, theme, or build your own UI with the builder API
- π‘οΈ Type-safe Errors - No more catching
FirebaseAuthExceptionand parsing codes - π i18n Ready - All strings customizable. Use your own translation system
π― Supported Auth Methods #
| Provider | Sign In | Link Account | Status |
|---|---|---|---|
| π Anonymous | β | β | Built-in |
| π Google | β | β | Fully supported |
| π Apple | β | β | iOS/macOS |
| π§ Email/Password | β | β | Fully supported |
π Latest Enhancements #
- ποΈ Account Deletion with automatic reauthentication
- π Smart Credential Handling - If a provider is already linked elsewhere, offer to sign in
- π§ͺ Debug Mode - Long-press email field for test credentials
- π£ Lifecycle Callbacks - Run code before/after sign-out or deletion
πΈ Screenshots #
π Account Linking Flow #
| Profile with Badge | Link Options Sheet | Email Form | Settings Integration |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| User profile showing "Linked account" badge and "Link my account" button | Clean bottom sheet with Google, Apple, and Email options | Expandable email/password form with validation | Settings page with "Link Account" entry and "Recommended" badge |
π¨ What You Get Out of the Box #
- β Beautiful bottom sheets with handle bar and rounded corners
- β Smart error handling with inline error messages
- β Loading states with spinners on buttons
- β Expandable forms for email/password
- β Native platform look (Apple button black on iOS)
- β Dark mode support automatically
- β Consistent spacing and typography
- β Accessible with proper labels
β‘ Quick Start #
1οΈβ£ Install #
flutter pub add layou_auth
2οΈβ£ Initialize #
import 'package:layou_auth/layou_auth.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
LayouAuth.initialize(
config: LayouAuthConfig(
providers: [
GoogleProviderConfig(iosClientId: DefaultFirebaseOptions.ios.iosClientId),
const AppleProviderConfig(),
const EmailProviderConfig(),
],
),
);
runApp(const ProviderScope(child: MyApp()));
}
3οΈβ£ Use #
// Show auth bottom sheet
LayouAuthSheet.show(context, mode: LayouAuthMode.signIn);
That's it! π You have a complete auth system.
π When to Prompt Account Linking #
// β
After a purchase (they paid, they see the value)
void onPurchaseComplete() {
LayouAuthSheet.show(
context,
mode: LayouAuthMode.link,
strings: LayouAuthStrings(
linkAccountTitle: "Secure your purchase!",
linkAccountSubtitle: "Link your account to access on all devices.",
),
);
}
// β
After meaningful engagement (they're invested)
void onFirstContentCreated() {
if (LayouAuth.currentUser?.isAnonymous == true) {
LayouAuthSheet.show(context, mode: LayouAuthMode.link);
}
}
// β On app launch (they haven't seen value yet)
void initState() {
// DON'T: Force login wall before showing any value
// DO: Let them explore anonymously, prompt later
}
π‘ Examples #
Sign In #
LayouAuthSheet.show(
context,
mode: LayouAuthMode.signIn,
onSuccess: (user, method) {
print('Signed in with ${method.name}: ${user.email}');
},
);
Link Anonymous Account #
// Perfect for converting free users to permanent accounts
LayouAuthSheet.show(
context,
mode: LayouAuthMode.link,
onSuccess: (user, method) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Account linked with ${method.name}!')),
);
},
);
Settings Page Integration #
// In your settings page
Card(
child: ListTile(
leading: const Icon(Icons.link),
title: const Text('Link Account'),
subtitle: const Text('Secure your notes by linking with Google, Apple, or Email'),
trailing: const Chip(label: Text('Recommended')),
onTap: () => LayouAuthSheet.show(context, mode: LayouAuthMode.link),
),
)
Delete Account with Confirmation #
// New! With automatic reauthentication
ElevatedButton(
onPressed: () async {
final confirmed = await LayouDeleteAccountSheet.show(
context,
title: 'Delete Account?',
message: 'This will permanently delete your data.',
onBeforeDelete: () async {
// Delete user documents
await firestore.collection('users').doc(userId).delete();
},
);
if (confirmed == true) {
// User deleted and automatically logged out
context.go(Routes.welcome);
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Delete My Account'),
);
Sign Out with Callbacks #
// Clean up before/after logout
final service = ref.read(layouAuthServiceProvider);
await service.signOut(
onBeforeLogout: () async {
// Cancel subscriptions, clear cache, etc.
await subscriptionService.cancelAll();
},
onAfterLogout: () async {
// Navigate to home, track analytics, etc.
analytics.logLogout();
context.go(Routes.home);
},
);
π― Advanced Usage #
Custom Strings (i18n) #
LayouAuthSheet.show(
context,
mode: LayouAuthMode.link,
strings: LayouAuthStrings(
linkAccountTitle: t.auth.linkTitle,
linkAccountSubtitle: t.auth.linkSubtitle,
googleButton: t.auth.continueWithGoogle,
appleButton: t.auth.continueWithApple,
emailButton: t.auth.continueWithEmail,
closeButton: t.common.close,
),
);
Custom Theme #
LayouAuthSheet.show(
context,
theme: LayouAuthTheme(
buttonBorderRadius: 16.0,
buttonSpacing: 16.0,
googleButtonStyle: OutlinedButton.styleFrom(...),
appleButtonStyle: ElevatedButton.styleFrom(...),
),
);
Full Custom UI (Builder) #
LayouAuthSheet.show(
context,
builder: (context, state, actions) {
return YourCustomWidget(
isLoading: state.isLoading,
error: state.error,
onGoogleTap: actions.linkWithGoogle,
onAppleTap: actions.linkWithApple,
);
},
);
Individual Buttons #
LayouGoogleButton(
label: 'Continue with Google',
isLoading: _isLoading,
onPressed: () async {
final result = await ref.read(layouAuthActionsProvider.notifier).signInWithGoogle();
result.when(
success: (user) => print('Success: ${user.email}'),
error: (error) => print('Error: ${error.message}'),
);
},
)
Inline Section (Settings) #
LayouAuthSection(
title: 'Link your account',
subtitle: 'Keep your data safe',
showGoogle: true,
showApple: true,
showEmail: true,
onSuccess: (user, method) {
showSuccessSnackbar('Account linked!');
},
)
π§ͺ Debug Mode Features #
Auto-fill Test Credentials #
In debug mode, long-press the email field to generate test credentials:
LayouEmailForm(
onSubmit: _handleSubmit,
debugCredentialsMessage: 'Test credentials generated!', // Optional snackbar
)
Result:
- Email:
test_XXX@yopmail.com(random number) - Password:
azerty123
Perfect for rapid testing without typing!
π Riverpod Providers #
Access auth state and actions anywhere in your app:
// Current user
final user = ref.watch(layouCurrentUserProvider);
if (user != null) {
print('Logged in: ${user.email}');
}
// Auth state stream
ref.listen(layouAuthStateProvider, (previous, next) {
if (next != null) {
print('User signed in');
}
});
// Check auth status
final isAuthenticated = ref.watch(layouIsAuthenticatedProvider);
final isAnonymous = ref.watch(layouIsAnonymousProvider);
// Check linked providers
final hasGoogle = ref.watch(layouHasGoogleProvider);
final hasApple = ref.watch(layouHasAppleProvider);
final hasEmail = ref.watch(layouHasEmailProvider);
final linkedProviders = ref.watch(layouLinkedProvidersProvider);
// Auth actions
final actions = ref.read(layouAuthActionsProvider.notifier);
await actions.signInWithGoogle();
await actions.linkWithApple();
await actions.signOut();
await actions.deleteUser();
π‘οΈ Error Handling #
All errors are type-safe. No more parsing Firebase error codes!
final result = await actions.signInWithGoogle();
result.when(
success: (user) => print('Welcome ${user.email}'),
error: (e) => switch (e) {
UserCancelledException() => null, // User cancelled, do nothing
CredentialAlreadyInUseException() => showCredentialInUseDialog(),
NetworkException() => showError('Check your connection'),
UserNotFoundException() => showError('No account found'),
WrongPasswordException() => showError('Incorrect password'),
WeakPasswordException() => showError('Password too weak'),
_ => showError('Something went wrong'),
},
);
Available Exceptions #
| Exception | When it happens |
|---|---|
UserCancelledException |
User cancelled Google/Apple sign-in |
NoUserException |
No authenticated user found |
SignInFailedException |
Generic sign-in failure |
LinkingFailedException |
Generic linking failure |
CredentialAlreadyInUseException |
Provider already linked elsewhere |
EmailAlreadyInUseException |
Email already registered |
WeakPasswordException |
Password too weak |
InvalidEmailException |
Invalid email format |
UserNotFoundException |
User not found (sign-in) |
WrongPasswordException |
Incorrect password |
UserDisabledException |
Account disabled |
NetworkException |
Network error |
RequiresRecentLoginException |
Sensitive operation needs reauth |
UnknownAuthException |
Unknown error |
π Smart Credential Handling #
New! When linking a provider that's already used on another account, LayouAuth offers to sign in instead:
// User tries to link Google account
// But that Google account exists on another account
// β LayouAuth shows: "Account Already Exists. Sign in instead?"
// β User clicks "Sign In"
// β Seamlessly switches to the existing account
Customize the messaging:
LayouAuthSheet.show(
context,
mode: LayouAuthMode.link,
credentialInUseTitle: 'Account Exists',
credentialInUseMessage: 'This {provider} is already registered. Sign in?',
credentialInUseSignInButton: 'Yes, Sign In',
credentialInUseCancelButton: 'No, Thanks',
);
π£ Lifecycle Hooks #
Run custom code at key moments:
Global Hooks (in config) #
LayouAuth.initialize(
config: LayouAuthConfig(
providers: [...],
onSignedIn: (user, method) async {
// Create user in Firestore
await firestore.collection('users').doc(user.uid).set({
'email': user.email,
'signInMethod': method.name,
'createdAt': FieldValue.serverTimestamp(),
});
// Track analytics
analytics.logSignIn(method: method.name);
},
onAccountLinked: (user, method) async {
// Award bonus for linking account
await creditsService.addBonus(5);
analytics.logAccountLinked(method: method.name);
},
onSignedOut: () async {
// Clear local cache
await cacheService.clearAll();
},
onUserDeleted: (uid) async {
// Cloud Functions handles Firestore cleanup
// But you can do client-side cleanup here
await localDb.clear();
},
),
);
Per-operation Callbacks #
// Sign out
await service.signOut(
onBeforeLogout: () async {
// Cancel subscriptions
await subscriptionService.cancelAll();
},
onAfterLogout: () async {
// Navigate
context.go(Routes.home);
},
);
// Delete account
await service.deleteUser(
onBeforeDelete: () async {
// Delete user-specific data
await deleteUserDocuments();
},
onAfterDelete: () async {
// Analytics
analytics.logAccountDeleted();
},
);
π§ Platform Setup #
π± iOS/macOS (Apple Sign-In)
-
Enable capability in Xcode:
- Open
ios/Runner.xcworkspace - Select your target β Signing & Capabilities
- Click
+ Capabilityβ Add "Sign in with Apple"
- Open
-
Configure in Apple Developer Console:
- Go to Certificates, Identifiers & Profiles
- Select your App ID β Enable "Sign in with Apple"
-
Add to config:
const AppleProviderConfig()
π Google Sign-In
-
Add configuration files:
- iOS: Add
GoogleService-Info.plisttoios/Runner/ - Android: Add
google-services.jsontoandroid/app/
- iOS: Add
-
Get iOS client ID:
GoogleProviderConfig( iosClientId: DefaultFirebaseOptions.ios.iosClientId, // Or manually: 'YOUR_IOS_CLIENT_ID.apps.googleusercontent.com' ) -
Enable in Firebase Console:
- Authentication β Sign-in method β Google β Enable
π§ Email/Password
No platform setup needed! Just enable in Firebase:
-
Firebase Console β Authentication β Sign-in method β Email/Password β Enable
-
Optional config:
const EmailProviderConfig( passwordMinLength: 8, // Default: 6 )
π Documentation #
Core Concepts #
- Sign In: Authenticate a user (creates new session)
- Link Account: Attach a provider to existing anonymous user
- Anonymous User: Temporary user without credentials
- Permanent User: User with at least one auth provider linked
Common Patterns #
1. Start Anonymous, Link Later (Freemium)
// App start - create anonymous user
await LayouAuth.auth.signInAnonymously();
// User wants premium features - prompt to link
if (isPremiumFeature && isAnonymous) {
LayouAuthSheet.show(
context,
mode: LayouAuthMode.link,
onSuccess: (user, method) {
enablePremiumFeatures();
},
);
}
2. Force Link Before Action
// User tries to save data
Future<void> saveNote() async {
if (ref.read(layouIsAnonymousProvider)) {
// Must link first
final result = await LayouAuthSheet.show(
context,
mode: LayouAuthMode.link,
);
if (result != true) return; // User cancelled
}
// Now safe to save
await notesService.save(note);
}
3. Settings Page with Recommended Badge
// Show "Recommended" badge if anonymous
final isAnonymous = ref.watch(layouIsAnonymousProvider);
ListTile(
title: const Text('Link Account'),
subtitle: const Text('Secure your data across devices'),
trailing: isAnonymous
? const Chip(label: Text('Recommended'))
: null,
onTap: () => LayouAuthSheet.show(context, mode: LayouAuthMode.link),
)
π€ Contributing #
Contributions are welcome! Please read CONTRIBUTING.md first.
Development Setup #
# Clone the repo
git clone https://github.com/yelkamel/layou_auth.git
# Install dependencies
flutter pub get
# Run tests
flutter test
# Run example app
cd example
flutter run
Reporting Issues #
Found a bug? Open an issue with:
- Flutter version
- Device/OS
- Steps to reproduce
- Expected vs actual behavior
π Roadmap #
- β Phone authentication
- β Multi-factor authentication
- β Password reset UI
- β Email verification UI
- β Anonymous user migration tools
- β Biometric authentication
- β Session management UI
π Credits #
Built with β€οΈ by the Layou team.
Special thanks to:
- firebase_auth - The foundation
- google_sign_in - Google integration
- sign_in_with_apple - Apple integration
- riverpod - State management
π License #
MIT License - see LICENSE file for details.
π Show Your Support #
If LayouAuth saved you time, give it a βοΈ on GitHub and a π on pub.dev!
Made with β€οΈ for the Flutter community



