Skip to main content

forui_firebase_ui_auth 0.1.0 copy "forui_firebase_ui_auth: ^0.1.0" to clipboard
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.

example/lib/main.dart

// 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),
        ],
      ),
    );
  }
}