forui_firebase_ui_auth 0.1.0
forui_firebase_ui_auth: ^0.1.0 copied to clipboard
Pre-built widgets library that are integrated with the variety of the Firebase Auth providers.
// Copyright 2022, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:firebase_auth/firebase_auth.dart'
hide PhoneAuthProvider, EmailAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:forui_firebase_ui_auth/forui_firebase_ui_auth.dart';
import 'package:forui_firebase_ui_localizations/forui_firebase_ui_localizations.dart';
import 'package:forui_firebase_ui_oauth_apple/forui_firebase_ui_oauth_apple.dart';
import 'package:forui_firebase_ui_oauth_facebook/forui_firebase_ui_oauth_facebook.dart';
import 'package:forui_firebase_ui_oauth_google/forui_firebase_ui_oauth_google.dart';
import 'package:forui_firebase_ui_oauth_twitter/forui_firebase_ui_oauth_twitter.dart';
import 'package:flutter/foundation.dart';
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:forui/forui.dart';
import 'package:material_ui/material_ui.dart';
import 'config.dart';
import 'decorations.dart';
import 'firebase_options.dart';
final actionCodeSettings = ActionCodeSettings(
url: 'https://flutterfire-e2e-tests.firebaseapp.com',
handleCodeInApp: true,
androidMinimumVersion: '1',
androidPackageName: 'io.flutter.plugins.firebase_ui_example',
iOSBundleId: 'io.flutter.plugins.fireabaseUiExample',
);
final emailLinkProviderConfig = EmailLinkAuthProvider(
actionCodeSettings: actionCodeSettings,
);
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// await FirebaseAuth.instance.useAuthEmulator('localhost', 9099);
FirebaseUIAuth.configureProviders([
EmailAuthProvider(),
emailLinkProviderConfig,
PhoneAuthProvider(),
GoogleProvider(clientId: GOOGLE_CLIENT_ID),
AppleProvider(),
FacebookProvider(clientId: FACEBOOK_CLIENT_ID),
// apiKey and apiSecretKey are only used on macOS and Windows. Android and
// iOS sign in through Firebase, which holds the credentials itself.
TwitterProvider(
apiKey: TWITTER_API_KEY,
apiSecretKey: TWITTER_API_SECRET_KEY,
),
]);
final launchedFromSignInLink = await emailLinkProviderConfig
.isLaunchedFromSignInLink();
runApp(FirebaseAuthUIExample(launchedFromSignInLink: launchedFromSignInLink));
}
// Overrides a label for en locale
// To add localization for a custom language follow the guide here:
// https://flutter.dev/docs/development/accessibility-and-localization/internationalization#an-alternative-class-for-the-apps-localized-resources
class LabelOverrides extends DefaultLocalizations {
const LabelOverrides();
@override
String get emailInputLabel => 'Enter your email';
}
class FirebaseAuthUIExample extends StatelessWidget {
const FirebaseAuthUIExample({super.key, this.launchedFromSignInLink = false});
/// Whether an email sign in link launched the app, so it can complete the
/// sign in on the email link screen.
final bool launchedFromSignInLink;
String get initialRoute {
final user = FirebaseAuth.instance.currentUser;
if (launchedFromSignInLink && (user == null || user.isAnonymous)) {
return '/email-link-sign-in';
}
return switch (user) {
null => '/',
User(emailVerified: false, email: final String _) => '/verify-email',
_ => '/profile',
};
}
@override
Widget build(BuildContext context) {
final mfaAction = AuthStateChangeAction<MFARequired>((
context,
state,
) async {
final nav = Navigator.of(context);
try {
await startMFAVerification(resolver: state.resolver, context: context);
} on Exception {
// User cancelled MFA or verification failed after onError handling.
return;
}
// Clear sign-in + any residual MFA routes so the SMS screen cannot stick.
nav.pushNamedAndRemoveUntil('/profile', (route) => false);
});
return MaterialApp(
builder: (context, child) =>
FTheme(data: FTheme.neutral.light.touch, child: child!),
initialRoute: initialRoute,
routes: {
'/': (context) {
return SignInScreen(
actions: [
ForgotPasswordAction((context, email) {
Navigator.pushNamed(
context,
'/forgot-password',
arguments: {'email': email},
);
}),
VerifyPhoneAction((context, _) {
Navigator.pushNamed(context, '/phone');
}),
AuthStateChangeAction((context, state) {
final user = switch (state) {
SignedIn(user: final user) => user,
CredentialLinked(user: final user) => user,
UserCreated(credential: final cred) => cred.user,
_ => null,
};
switch (user) {
case User(emailVerified: false, email: final String _):
Navigator.pushNamed(context, '/verify-email');
// Providers are not obliged to return an email. Twitter only
// does when the app requests it, and such a user is never
// emailVerified, so without this they matched no case and
// the screen sat there after a successful sign in.
case User():
Navigator.pushReplacementNamed(context, '/profile');
case null:
break;
}
}),
mfaAction,
EmailLinkSignInAction((context) {
Navigator.pushReplacementNamed(context, '/email-link-sign-in');
}),
],
styles: const {
EmailFormStyle(signInButtonVariant: ButtonVariant.filled),
},
headerBuilder: headerImage('assets/images/flutterfire_logo.png'),
sideBuilder: sideImage('assets/images/flutterfire_logo.png'),
subtitleBuilder: (context, action) {
final actionText = switch (action) {
AuthAction.signIn => 'Please sign in to continue.',
AuthAction.signUp => 'Please create an account to continue',
_ => throw Exception('Invalid action: $action'),
};
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text('Welcome to Firebase UI! $actionText.'),
);
},
footerBuilder: (context, action) {
final actionText = switch (action) {
AuthAction.signIn => 'signing in',
AuthAction.signUp => 'registering',
_ => throw Exception('Invalid action: $action'),
};
return Column(
children: [
if (defaultTargetPlatform == TargetPlatform.iOS)
const AppTrackingTransparencyCard(),
Center(
child: Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
'By $actionText, you agree to our terms and conditions.',
style: TextStyle(
color: context.theme.colors.mutedForeground,
),
),
),
),
],
);
},
);
},
'/verify-email': (context) {
return EmailVerificationScreen(
headerBuilder: headerIcon(FLucideIcons.badgeCheck),
sideBuilder: sideIcon(FLucideIcons.badgeCheck),
actionCodeSettings: actionCodeSettings,
actions: [
EmailVerifiedAction(() {
Navigator.pushReplacementNamed(context, '/profile');
}),
AuthCancelledAction((context) {
FirebaseUIAuth.signOut(context: context);
Navigator.pushReplacementNamed(context, '/');
}),
],
);
},
'/phone': (context) {
return PhoneInputScreen(
actions: [
SMSCodeRequestedAction((context, action, flowKey, phone) {
Navigator.of(context).pushReplacementNamed(
'/sms',
arguments: {
'action': action,
'flowKey': flowKey,
'phone': phone,
},
);
}),
],
headerBuilder: headerIcon(FLucideIcons.phone),
sideBuilder: sideIcon(FLucideIcons.phone),
);
},
'/sms': (context) {
final arguments =
ModalRoute.of(context)?.settings.arguments
as Map<String, dynamic>?;
return SMSCodeInputScreen(
actions: [
AuthStateChangeAction<SignedIn>((context, state) {
Navigator.of(context).pushReplacementNamed('/profile');
}),
// A first phone sign-in is UserCreated, not SignedIn.
AuthStateChangeAction<UserCreated>((context, state) {
Navigator.of(context).pushReplacementNamed('/profile');
}),
],
flowKey: arguments?['flowKey'],
action: arguments?['action'],
headerBuilder: headerIcon(FLucideIcons.messageSquare),
sideBuilder: sideIcon(FLucideIcons.messageSquare),
);
},
'/forgot-password': (context) {
final arguments =
ModalRoute.of(context)?.settings.arguments
as Map<String, dynamic>?;
return ForgotPasswordScreen(
email: arguments?['email'],
headerMaxExtent: 200,
headerBuilder: headerIcon(FLucideIcons.lock),
sideBuilder: sideIcon(FLucideIcons.lock),
);
},
'/email-link-sign-in': (context) {
return EmailLinkSignInScreen(
actions: [
AuthStateChangeAction((context, state) {
if (state is SignedIn ||
state is UserCreated ||
state is CredentialLinked) {
Navigator.pushReplacementNamed(context, '/profile');
}
}),
],
provider: emailLinkProviderConfig,
headerMaxExtent: 200,
headerBuilder: headerIcon(FLucideIcons.link),
sideBuilder: sideIcon(FLucideIcons.link),
);
},
'/profile': (context) {
return ProfileScreen(
actions: [
SignedOutAction((context) {
Navigator.pushReplacementNamed(context, '/');
}),
mfaAction,
],
actionCodeSettings: actionCodeSettings,
showMFATile:
kIsWeb ||
defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.android,
showUnlinkConfirmationDialog: true,
showDeleteConfirmationDialog: true,
);
},
},
title: 'Firebase UI demo',
debugShowCheckedModeBanner: false,
supportedLocales: const [Locale('en')],
localizationsDelegates: [
FirebaseUILocalizations.withDefaultOverrides(const LabelOverrides()),
...GlobalMaterialLocalizations.delegates,
FirebaseUILocalizations.delegate,
],
);
}
}
class AppTrackingTransparencyCard extends StatefulWidget {
const AppTrackingTransparencyCard({super.key});
@override
State<AppTrackingTransparencyCard> createState() =>
_AppTrackingTransparencyCardState();
}
class _AppTrackingTransparencyCardState
extends State<AppTrackingTransparencyCard> {
bool _isAllowed = false;
@override
void initState() {
super.initState();
_checkTrackingStatus();
}
Future<void> _checkTrackingStatus() async {
try {
final status = await AppTrackingTransparency.trackingAuthorizationStatus;
setState(() {
_isAllowed = status == TrackingStatus.authorized;
});
} catch (e) {
// Handle error silently
}
}
Future<void> _onToggleChanged(bool value) async {
if (value && !_isAllowed) {
// Request permission when toggling on
try {
final status =
await AppTrackingTransparency.requestTrackingAuthorization();
if (mounted) {
setState(() {
_isAllowed = status == TrackingStatus.authorized;
});
if (status != TrackingStatus.authorized) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Tracking permission denied. Enable in Settings > Privacy & Security > Tracking',
),
duration: Duration(seconds: 4),
),
);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error requesting permission: $e')),
);
}
}
} else if (!value && _isAllowed) {
// Can't turn off programmatically - show message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'To disable tracking, go to Settings > Privacy & Security > Tracking',
),
duration: Duration(seconds: 3),
),
);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('App tracking allowed'),
const SizedBox(width: 12),
Switch(value: _isAllowed, onChanged: _onToggleChanged),
],
),
);
}
}