Sign in UI

So the time has finally come to talk about the UI. We're going to start with the login and register screens. Of course you can find the full code at https://github.com/cadaniel/FCast

Authentication Routing

Before we can make our screens, we need to have a division in the app. When you are logged out, you see the sign in and register screen, and when you are logged in, you see the home screen. Right now the home screen is going to be a placeholder. Since I put state in cubits using flutter_bloc, this seems like a good place to start. Under lib/src/ui/main/cubit/ we have our authentication_state.dart and authentication_cubit.dart

@freezed
abstract class AuthenticationState with _$AuthenticationState {
  factory AuthenticationState.loading() = Loading;
  factory AuthenticationState.signedOut() = SignedOut;
  factory AuthenticationState.signedIn() = SignedIn;
}

and

@injectable
class AuthenticationCubit extends Cubit<AuthenticationState> {
  final AuthService authService;

  AuthenticationCubit(this.authService) : super(AuthenticationState.loading()) {
    initAuth();
  }

  Future<void> initAuth() async {
    var isSignedIn = await authService.isSignedIn();
    if (isSignedIn) {
      emit(AuthenticationState.signedIn());
    } else {
      emit(AuthenticationState.signedOut());
    }
  }

  Future<void> _signedIn() async {
    emit(AuthenticationState.signedIn());
  }

  Future<bool> signInWithEmail(String email, String password) async {
    var result = await authService.signInWithEmailAndPassword(email, password);
    if (result) {
      _signedIn();
    }
    return result;
  }

  Future<bool> registerWithEmail(
      String email, String password, String confirmPassword) async {
    var result = await authService.registerUserWithEmailAndPassword(
        email, password, confirmPassword);
    if (result) {
      _signedIn();
    }
    return result;
  }

  Future<void> signInWithGoogle() async {
    var result = await authService.signInWithGoogle();
    if (result) {
      _signedIn();
    }
  }

  Future<void> signInWithApple() async {
    var result = await authService.signInWithApple();
    if (result) {
      _signedIn();
    }
  }

  Future<void> signOut() async {
    await authService.signOut();
    emit(AuthenticationState.signedOut());
  }
}

So our state contains 3 states, loading (while we check our initial state), logged out, and logged in. Our cubit is also responsible for signing in with the various methods. These are called out to the repositories. We'll cover the sign in with google and apple in a moment.

Routing

If you haven't read my post on the Navigator 2.0 in flutter, check it out at whitewhiskywolf.com/flutter-navigator-v2-part-1  I go into more details on the mechanics of how the navigator works over there. Next up let's look at our app entry point and add our app.

class Main extends StatelessWidget {
  static final textColor = Colors.white;
  final themeData = ThemeData(
    brightness: Brightness.dark,
    backgroundColor: Color(0xff124559),
    scaffoldBackgroundColor: Color(0xff124559),
    canvasColor: Color(0xff124559),
    buttonTheme: ButtonThemeData(
      buttonColor: Color(0xff734B5E),
    ),
    iconTheme: IconThemeData(color: Colors.white),
    textTheme: GoogleFonts.latoTextTheme(
      TextTheme(
        headline1: TextStyle(color: textColor, fontSize: 42),
        headline2: TextStyle(color: textColor, fontSize: 36),
        headline3: TextStyle(color: textColor, fontSize: 24),
        headline4: TextStyle(color: textColor, fontSize: 21),
        headline5: TextStyle(color: textColor, fontSize: 15),
        bodyText1: TextStyle(color: textColor, fontSize: 24),
        bodyText2: TextStyle(color: textColor, fontSize: 18),
        caption: TextStyle(color: Color(0xff8585ad), fontSize: 12),
        button: TextStyle(
            color: Color(0xffFAD703),
            fontSize: 18,
            fontWeight: FontWeight.bold),
      ),
    ),
  );

  final cupertinoTheme = CupertinoThemeData(
    primaryColor: Color(0xff124559),
    barBackgroundColor: Color(0xff124559),
    scaffoldBackgroundColor: Color(0xff124559),
    brightness: Brightness.dark,
  );

  Widget _buildApp(BuildContext context) {
    if (Platform.isIOS) {
      return Theme(
        data: themeData,
        isMaterialAppTheme: true,
        child: CupertinoApp.router(
          localizationsDelegates: [
            DefaultMaterialLocalizations.delegate,
            DefaultCupertinoLocalizations.delegate,
            DefaultWidgetsLocalizations.delegate,
          ],
          theme: cupertinoTheme,
          debugShowCheckedModeBanner: false,
          routeInformationParser: AuthenticationInformationParser(),
          routerDelegate: AuthenticationRouterDelegate(),
        ),
      );
    } else {
      return MaterialApp.router(
        themeMode: ThemeMode.dark,
        theme: themeData,
        debugShowCheckedModeBanner: false,
        routeInformationParser: AuthenticationInformationParser(),
        routerDelegate: AuthenticationRouterDelegate(),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => GetIt.instance.get<AuthenticationCubit>(),
      child: _buildApp(context),
    );
  }
}

It looks like there's a lot here, but a lot of it is fluff. Starting at the bottom we have our standard build method, which injects the cubit into the widget tree using BlocProvider and then calls _buildApp. From there we either display a CupertinoApp.router or a MaterialApp.router depending on the platform. The theme data is just to style the app globally. We also use AuthenticationInformationParser and AuthenticationRouterDelegate. Let's look at those next. They live under /lib/src/ui/main/navigation.

class AuthenticationInformationParser
    extends RouteInformationParser<AuthenticationState> {
  @override
  Future<AuthenticationState> parseRouteInformation(
      RouteInformation routeInformation) {
    return Future.value(AuthenticationState.loading());
  }

  @override
  RouteInformation restoreRouteInformation(AuthenticationState path) {
    return RouteInformation(location: "/");
  }
}

Here we have a simple class that just returns the loading state. When we make a web app, we'll expand on this then. The real meat and potatoes is in the delegate.

class AuthenticationRouterDelegate extends RouterDelegate<AuthenticationState>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<AuthenticationState> {
  final GlobalKey<NavigatorState> navigatorKey;
  AuthenticationState _currentState = AuthenticationState.loading();

  AuthenticationRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();

  List<Page<dynamic>> getPages() {
    List<Page<dynamic>> pages = [];
    _currentState.when(
      loading: () => pages.add(SplashPage()),
      signedOut: () => pages.add(AuthenticationPage()),
      signedIn: () => pages.add(HomePage()),
    );
    return pages;
  }

  @override
  Widget build(BuildContext context) {
    var cubit = BlocProvider.of<AuthenticationCubit>(context);
    _currentState = cubit.state;
    return BlocListener(
      cubit: cubit,
      listener: (_, AuthenticationState state) {
        _currentState = state;
        notifyListeners();
      },
      child: Navigator(
        pages: getPages(),
        onPopPage: (result, route) => false,
      ),
    );
  }

  @override
  Future<void> setNewRoutePath(AuthenticationState configuration) async {
    _currentState = configuration;
    notifyListeners();
  }
}

Just like I talked about in the other post linked above, we listen to the AuthenctiationCubit state, and configure our route based on that. We're now ready for the SignInPage. We'll notice getPages returns only the page we're interested in. We have no back navigation to worry about. We also notice onPopPage always returns false, since we have no back navigation to go to.

Sign In and Log In

So to start we need a base. We'll be working under lib/src/ui/login/[cubit, navigation, ui] for the appropriate files. Let's start with the authentication_page.dart

class AuthenticationPage extends PlatformPage {
  AuthenticationPage() : super(AuthenticationWidget(), "/auth");
}

class AuthenticationWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => GetIt.instance.get<SignInCubit>(),
      child: Router(
        routerDelegate: SignInRouteDelegate(),
        routeInformationParser: SignInRouteInformationParser(),
      ),
    );
  }
}

We're doing nested navigation, so we have to put a Router into the widget tree. For details on the information parser, check it out on github, but let's look at the delegate.

class SignInRouteDelegate extends RouterDelegate<SignInState>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<SignInState> {
  final GlobalKey<NavigatorState> navigatorKey;
  SignInState _currentState = SignInState.selectLogin();

  SignInRouteDelegate() : navigatorKey = GlobalKey<NavigatorState>();

  List<Page<dynamic>> getPages() {
    List<Page<dynamic>> pages = [SelectLoginPage()];
    _currentState.maybeWhen(
      orElse: () {},
      login: () => pages.add(LoginWithEmailPage()),
      register: () => pages.add(RegisterWithEmailPage()),
    );
    return pages;
  }

  @override
  Widget build(BuildContext context) {
    return BlocListener(
      cubit: BlocProvider.of<SignInCubit>(context),
      listener: (_, SignInState state) {
        _currentState = state;
        notifyListeners();
      },
      child: Navigator(
        pages: getPages(),
        onPopPage: (route, result) {
          if (!route.didPop(result)) {
            return false;
          }
          return _currentState.when(
            selectLogin: () => false,
            login: () => true,
            register: () => true,
          );
        },
      ),
    );
  }

  @override
  Future<void> setNewRoutePath(SignInState configuration) async {
    _currentState = configuration;
    notifyListeners();
  }
}

We're following the same pattern as before. Get pages takes a look at the current state, and returns a list of pages that can be navigated. We also still add the bloc listener to change state based on what events get put through the bloc. Lastly the onPopPage looks a little more complex. We see that it that if the page didn't pop, we return false. Otherwise we look at the current state. If we're already at the base selectLogin we return false, otherwise true.

UI

Select Login

Now we're ready to look at the UI. Let's start with the select login page.

class SelectLoginPage extends PlatformPage {
  SelectLoginPage()
      : super(
          SelectLoginWidget(),
          "select login",
          title: "Select Login",
        );
}

class SelectLoginWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var signInCubit = BlocProvider.of<SignInCubit>(context);
    var authCubit = BlocProvider.of<AuthenticationCubit>(context);
    return PlatformScaffold(
      child: Container(
        margin: EdgeInsets.all(8),
        width: double.infinity,
        child: Flex(
          direction: Axis.vertical,
          children: [
            Expanded(
              flex: 3,
              child: Center(
                child: FaIcon(
                  FontAwesomeIcons.podcast,
                  size: 200,
                  color: Colors.white,
                ),
              ),
            ),
            Row(
              children: [
                Expanded(
                  child: Divider(
                    color: Colors.white,
                  ),
                ),
                Container(
                  margin: EdgeInsets.symmetric(horizontal: 8),
                  child: Text(
                    "Let's Get Started",
                    style: Theme.of(context).textTheme.bodyText1,
                  ),
                ),
                Expanded(
                  child: Divider(
                    color: Colors.white,
                  ),
                ),
              ],
            ),
            Flexible(
              flex: 1,
              child: Flex(
                direction: Axis.vertical,
                children: [
                  Flexible(
                    child: Center(
                      child: Container(
                        margin: EdgeInsets.symmetric(horizontal: 8),
                        width: double.infinity,
                        child: RaisedButton(
                          shape: StadiumBorder(),
                          onPressed: () => signInCubit.selectRegister(),
                          child: Text("Crate New Account"),
                        ),
                      ),
                    ),
                  ),
                  Flexible(
                    child: Center(
                      child: Container(
                        margin: EdgeInsets.symmetric(horizontal: 8),
                        width: double.infinity,
                        child: RaisedButton(
                          shape: StadiumBorder(),
                          onPressed: () => signInCubit.selectLogin(),
                          child: Text("Login With Email"),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
            Row(
              children: [
                Expanded(
                  child: Divider(
                    color: Colors.white,
                  ),
                ),
                Container(
                  margin: EdgeInsets.symmetric(horizontal: 8),
                  child: Text(
                    "OR",
                    style: Theme.of(context).textTheme.bodyText1,
                  ),
                ),
                Expanded(
                  child: Divider(
                    color: Colors.white,
                  ),
                ),
              ],
            ),
            Flexible(
              flex: 1,
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Flex(
                  direction: Axis.vertical,
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    Container(
                      width: double.infinity,
                      child: RaisedButton(
                        shape: StadiumBorder(),
                        color: Colors.white,
                        child: Text(
                          "Continue with Google",
                          style: Theme.of(context)
                              .textTheme
                              .bodyText2
                              .copyWith(color: Colors.black),
                        ),
                        onPressed: () => authCubit.signInWithGoogle(),
                      ),
                    ),
                    Container(
                      width: double.infinity,
                      child: RaisedButton(
                        shape: StadiumBorder(),
                        color: Colors.black,
                        child: Text(
                          "Continue with Apple",
                          style: Theme.of(context)
                              .textTheme
                              .bodyText2
                              .copyWith(color: Colors.white),
                        ),
                        onPressed: () => authCubit.signInWithApple(),
                      ),
                    ),
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

There's really nothing here worth going into extreme detail. I've used flexible to auto-size things based on screen size. This means it will take up portions relative to the flex value.

login

class LoginWithEmailWidget extends StatefulWidget {
  @override
  _LoginWithEmailWidgetState createState() => _LoginWithEmailWidgetState();
}

class _LoginWithEmailWidgetState extends State<LoginWithEmailWidget> {
  final _formKey = GlobalKey<FormState>();

  final _emailController = TextEditingController();

  final _passwordController = TextEditingController();

  String _errorText;

  @override
  Widget build(BuildContext context) {
    return PlatformScaffold(
      child: Container(
          margin: EdgeInsets.all(16),
          width: double.infinity,
          child: KeyboardVisibilityBuilder(
            builder: (BuildContext context, bool isKeyboardOpen) {
              return Flex(
                direction: Axis.vertical,
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Align(
                    alignment: Alignment.topLeft,
                    child: IconButton(
                      icon: PlatformIcon(
                        size: 24,
                        materialIcon: Icons.arrow_back,
                        cupertinoIcon: CupertinoIcons.back,
                      ),
                      onPressed: () => Navigator.pop(context),
                    ),
                  ),
                  Flexible(
                    flex: isKeyboardOpen ? 1 : 3,
                    child: FaIcon(
                      FontAwesomeIcons.podcast,
                      color: Colors.white,
                      size: isKeyboardOpen ? 100 : 200,
                    ),
                  ),
                  Flexible(
                    flex: 2,
                    child: SingleChildScrollView(
                      child: Form(
                        key: _formKey,
                        child: Column(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            if (_errorText != null && _errorText.isNotEmpty)
                              Text(
                                _errorText,
                                style: Theme.of(context)
                                    .textTheme
                                    .caption
                                    .copyWith(
                                        color: Theme.of(context).errorColor),
                              ),
                            TextFormField(
                              controller: _emailController,
                              keyboardType: TextInputType.emailAddress,
                              decoration: InputDecoration(
                                labelText: "Email",
                                floatingLabelBehavior:
                                    FloatingLabelBehavior.always,
                                border: OutlineInputBorder(),
                                focusedBorder: OutlineInputBorder(
                                  borderSide: BorderSide(color: Colors.white),
                                ),
                              ),
                              validator: (value) => value.isEmail()
                                  ? null
                                  : "Please enter a valid email",
                            ),
                            SizedBox(height: 8),
                            TextFormField(
                              controller: _passwordController,
                              obscureText: true,
                              decoration: InputDecoration(
                                labelText: "Password",
                                floatingLabelBehavior:
                                    FloatingLabelBehavior.always,
                                border: OutlineInputBorder(),
                                focusedBorder: OutlineInputBorder(
                                  borderSide: BorderSide(color: Colors.white),
                                ),
                              ),
                              validator: (value) => value.isNotEmpty
                                  ? null
                                  : "Please enter a password",
                            ),
                            Container(
                              width: double.infinity,
                              child: RaisedButton(
                                child: Text("Login"),
                                shape: StadiumBorder(),
                                onPressed: () async {
                                  if (_formKey.currentState.validate()) {
                                    var signInCubit =
                                        BlocProvider.of<AuthenticationCubit>(
                                            context);
                                    var result =
                                        await signInCubit.signInWithEmail(
                                            _emailController.text,
                                            _passwordController.text);
                                    if (!result) {
                                      setState(() {
                                        _errorText = "Unable to login";
                                      });
                                    }
                                  }
                                },
                              ),
                            )
                          ],
                        ),
                      ),
                    ),
                  ),
                  Align(
                    alignment: Alignment.bottomCenter,
                    child: InkWell(
                      child: Text(
                        "Need an account? Register Now!",
                        style: Theme.of(context)
                            .textTheme
                            .bodyText2
                            .copyWith(decoration: TextDecoration.underline),
                      ),
                      onTap: () {
                        var signInCubit = BlocProvider.of<SignInCubit>(context);
                        signInCubit.selectRegister();
                      },
                    ),
                  )
                ],
              );
            },
          )),
    );
  }
}

By in large we have a similar strategy for the login screen. The main thing to note here is we're using KeyboardVisibilityBuilder. This is a widget I use to detect if the keyboard is open, and resize some elements based on it.

class KeyboardVisibilityBuilder extends StatefulWidget {
  final Widget Function(
    BuildContext context,
    bool isKeyboardVisible,
  ) builder;

  const KeyboardVisibilityBuilder({
    Key key,
    @required this.builder,
  }) : super(key: key);

  @override
  _KeyboardVisibilityBuilderState createState() =>
      _KeyboardVisibilityBuilderState();
}

class _KeyboardVisibilityBuilderState extends State<KeyboardVisibilityBuilder>
    with WidgetsBindingObserver {
  var _isKeyboardVisible = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeMetrics() {
    final bottomInset = WidgetsBinding.instance.window.viewInsets.bottom;
    final newValue = bottomInset > 0.0;
    if (newValue != _isKeyboardVisible) {
      setState(() {
        _isKeyboardVisible = newValue;
      });
    }
  }

  @override
  Widget build(BuildContext context) => widget.builder(
        context,
        _isKeyboardVisible,
      );
}

Credit belongs to https://pub.dev/packages/flutter_keyboard_visibility for this. I don't feel the need to import the entire package, so I took the core component I needed, and copied it. If you need web support, it might be best to take a look at the package.

register

This looks very similar to the login widget, just with some extras for the confirm password

class RegisterWithEmailPage extends PlatformPage {
  RegisterWithEmailPage()
      : super(
          RegisterWithEmailWidget(),
          "register with email",
          title: "register",
        );
}

class RegisterWithEmailWidget extends StatefulWidget {
  @override
  _RegisterWithEmailWidgetState createState() =>
      _RegisterWithEmailWidgetState();
}

class _RegisterWithEmailWidgetState extends State<RegisterWithEmailWidget> {
  final _formKey = GlobalKey<FormState>();

  final _emailController = TextEditingController();

  final _passwordController = TextEditingController();

  final _confirmController = TextEditingController();

  String _errorText;

  @override
  Widget build(BuildContext context) {
    return PlatformScaffold(
      child: Container(
        margin: EdgeInsets.all(16),
        width: double.infinity,
        child: KeyboardVisibilityBuilder(
          builder: (BuildContext context, bool isKeyboardOpen) {
            return Flex(
              direction: Axis.vertical,
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Align(
                  alignment: Alignment.topLeft,
                  child: IconButton(
                    icon: PlatformIcon(
                      size: 24,
                      materialIcon: Icons.arrow_back,
                      cupertinoIcon: CupertinoIcons.back,
                    ),
                    onPressed: () => Navigator.pop(context),
                  ),
                ),
                Flexible(
                  flex: isKeyboardOpen ? 1 : 3,
                  child: FaIcon(
                    FontAwesomeIcons.podcast,
                    color: Colors.white,
                    size: isKeyboardOpen ? 100 : 200,
                  ),
                ),
                Flexible(
                  flex: 2,
                  child: SingleChildScrollView(
                    child: Form(
                      key: _formKey,
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          if (_errorText != null && _errorText.isNotEmpty)
                            Text(
                              _errorText,
                              style: Theme.of(context)
                                  .textTheme
                                  .caption
                                  .copyWith(
                                      color: Theme.of(context).errorColor),
                            ),
                          TextFormField(
                            controller: _emailController,
                            keyboardType: TextInputType.emailAddress,
                            decoration: InputDecoration(
                              labelText: "Email",
                              floatingLabelBehavior:
                                  FloatingLabelBehavior.always,
                              border: OutlineInputBorder(),
                              focusedBorder: OutlineInputBorder(
                                borderSide: BorderSide(color: Colors.white),
                              ),
                            ),
                            validator: (value) => value.isEmail()
                                ? null
                                : "Please enter a valid email",
                          ),
                          SizedBox(height: 8),
                          TextFormField(
                            controller: _passwordController,
                            obscureText: true,
                            decoration: InputDecoration(
                              labelText: "Password",
                              floatingLabelBehavior:
                                  FloatingLabelBehavior.always,
                              border: OutlineInputBorder(),
                              focusedBorder: OutlineInputBorder(
                                borderSide: BorderSide(color: Colors.white),
                              ),
                            ),
                            validator: (value) => value.isNotEmpty
                                ? null
                                : "Please enter a password",
                          ),
                          SizedBox(height: 8),
                          TextFormField(
                            controller: _confirmController,
                            obscureText: true,
                            decoration: InputDecoration(
                              labelText: "Confirm Password",
                              floatingLabelBehavior:
                                  FloatingLabelBehavior.always,
                              border: OutlineInputBorder(),
                              focusedBorder: OutlineInputBorder(
                                borderSide: BorderSide(color: Colors.white),
                              ),
                            ),
                            validator: (value) =>
                                value == _passwordController.text
                                    ? null
                                    : "Passwords do not match",
                          ),
                          Container(
                            width: double.infinity,
                            child: RaisedButton(
                              child: Text("Create Account"),
                              shape: StadiumBorder(),
                              onPressed: () async {
                                if (_formKey.currentState.validate()) {
                                  var signInCubit =
                                      BlocProvider.of<AuthenticationCubit>(
                                          context);
                                  var result =
                                      await signInCubit.registerWithEmail(
                                          _emailController.text,
                                          _passwordController.text,
                                          _confirmController.text);
                                  if (!result) {
                                    setState(() {
                                      _errorText = "Unable to register";
                                    });
                                  }
                                }
                              },
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                ),
                Align(
                  alignment: Alignment.bottomCenter,
                  child: InkWell(
                    child: Text(
                      "Already have an account? Login Now!",
                      style: Theme.of(context)
                          .textTheme
                          .bodyText2
                          .copyWith(decoration: TextDecoration.underline),
                    ),
                    onTap: () {
                      var signInCubit = BlocProvider.of<SignInCubit>(context);
                      signInCubit.selectLogin();
                    },
                  ),
                )
              ],
            );
          },
        ),
      ),
    );
  }
}

Sign in With Apple and Google

I promised I would get to these changes, and here I will. The authentication service will simply return what the repository returns.


  @override
  Future<bool> signInWithGoogle() => authRepository.signInWithGoogle();

  @override
  Future<bool> signInWithApple() => authRepository.signInWithApple();

  @override
  Future<void> signOut() => authRepository.signOut();

Next up we need to create an abstraction for the sign in with apple. There's some static calls that are difficult to mock, so we'll isolate that little bit of code in a different class so we can do a mock in the repository.

@injectable
class SignInAppleModule {
  final AuthVariables authVariables;

  SignInAppleModule(this.authVariables);
  
  Future<AuthorizationCredentialAppleID> getSignInWithAppleResult() async {
    return SignInWithApple.getAppleIDCredential(
      webAuthenticationOptions: WebAuthenticationOptions(
        clientId: authVariables.appleClientId,
        redirectUri: Uri.parse(
          authVariables.appleRedirectUri,
        ),
      ),
      scopes: [AppleIDAuthorizationScopes.email],
    );
  }
}

This simple class lives under a new directory lib/src/domain/repositories/modules. This static call uses the injected AuthVariables (more in a moment) to make this call. Under lib/src/config we have AuthVariables interface.

abstract class AuthVariables {
  String get appleClientId;
  String get appleRedirectUri;
}

We'll create a DevAuthVariables implementation

@Injectable(as: AuthVariables) //TODO env config
class DevAuthVariables extends AuthVariables {
  @override
  String get appleClientId => "com.whitewhiskywolf.fCast";

  @override
  String get appleRedirectUri =>
      "[URL]";
}

The todo means we'll be adding functionality later on to depend on the get_it environment. But that's for a later date. One last thing, we need something for sing in with Google, a new module under lib/src/injectable/module.

@module
abstract class GoogleSignInModule {
  GoogleSignIn get googleSignIn => GoogleSignIn(
        scopes: [
          'email',
        ],
      );
}

Now we're ready for the repository. First up, let's add some dependencies.

@Injectable(as: AuthRepository)
class FirebaseAuthRepository implements AuthRepository {
  final FirebaseAuthModule firebaseAuth;
  final GoogleSignIn googleSignIn;
  final SignInAppleModule signInAppleModule;

  FirebaseAuthRepository(
      this.firebaseAuth, this.googleSignIn, this.signInAppleModule);

Now we can actually work with the sign in methods

Future<bool> signInWithGoogle() async {
    var account = await googleSignIn.signIn();
    var auth = await account.authentication;
    if (auth == null) {
      return false;
    }

    var credential = GoogleAuthProvider.credential(
        accessToken: auth.accessToken, idToken: auth.idToken);

    var authResult =
        await firebaseAuth.firebaseAuth.signInWithCredential(credential);
    var user = authResult.user;
    if (user == null) {
      return false;
    }
    user.updateEmail(account.email);
    return true;
  }

  Future<bool> signInWithApple() async {
    try {
      var result = await signInAppleModule.getSignInWithAppleResult();

      if (result != null) {
        var oAuthProvider = OAuthProvider("apple.com");
        var credential = oAuthProvider.credential(
            idToken: result.identityToken,
            accessToken: result.authorizationCode);
        var authResult =
            await firebaseAuth.firebaseAuth.signInWithCredential(credential);
        var user = authResult.user;
        return user != null;
      }
      return false;
    } on SignInWithAppleAuthorizationException catch (e) {
      print(e.toString());
      return false;
    }
  }

These methods can be taken as is. They simply call out to the sign in method, then pass them to firebase. Lastly is the sign out

Future<void> signOut() async => firebaseAuth.firebaseAuth.signOut();

And there we have it! Sign in with email, Apple, and Google!

But wait, testing...

I have added unit test in the project, but I won't add them here. This post is long enough as is.

What's next?

Well that's a great questions! I'll be working with the new Flutter Integration Tests that was just released as 1.0.0. Want early access? Subscribe!

Subscribe

If you like this content, get email updates for new posts! There's a subscribe button at the top. Want to know more about what kinds of subscriptions are avaliable? Take a look at https://whitewhiskywolf.com/new-membership-and-subscription/ to learn more.

Casey Daniel

Casey Daniel

Canada
Tracy Pyett

Tracy Pyett