Passwordless sign-in with Firebase in Flutter — Part 1

Hashem Abounajmi
15 min readJun 21, 2023

--

Nowadays each app needs a form of user authentication, and a common flow is registering users with username and password. but it has some drawbacks:

Traditional way drawbacks:

  1. High friction of sign-up and sign-in
  2. Yet another password! people tend to reuse the same password and undermines the security of passwords
  3. Another step required to ensure user is the legitimate owner of email.

Now there is a better alternative that resolve above issues which is email link sign-in or passwordless sign-in method. in this method we will send the user a link by email and by tapping on the link they will be redirected to the app and will be sign-in seamlessly.

Password-less sign-in benefits:

  1. Quick and seamless sign-up and sign-in flow
  2. Increase rate of user registration dramatically
  3. No another password needed. reducing risk of exposing passwords.
  4. You have new users with genuine email addresses

Here you will learn:

  • Integrate Firebase in a Flutter project
  • Use Firebase email link (passwordless sign-in) feature.
  • Decouple navigation from UI which gives us flexibility that I will demonstrate during the tutorial.
  • The process of converting requirements to code step by step
  • Writing Tests

I have gone through small steps and practical approach and encourage you to follow and replicate, because you can learn by doing.

Let’s begin

First clone the project and checkout thestartertag :

git clone 
git checkout starter

run the project you will see a simple registration flow:

It’s just a simple UI that ask user for email and doesn’t have any logic. we will start to complete it together.

Passwordless sign-in flow

  1. User request a sign-in link by filling the email field and tapping on Sign-in button.
  2. User receives an email containing the sign-in link, user taps on the link and the app launches, link will be validated and user gets signed-in.

Lets think about API requirements before going deep into implementation. at this moment we can think of two API requirements:

  1. email the sign-in link

Future<void> sendSignInLinkToEmail(String email)

but there are two problem with the above interface:

  • the above signature doesn’t indicate which exceptions maybe thrown
  • the programmer is not forced to deal with errors

To solve the above issues, we leverage some functional programming data types which will be translated to below:

Future<Either<Failure, Unit>> sendSignInLinkToEmail(String email)

Either is functional programming data type can hold either Left or Right subtypes at a time. we use Left for failure case and Right for success. which in here will return Either Failure or Unit. Failure is our custom data type which we will define later and Unit is another function programming data type which represent void in object oriented programming.

2. sign-in with the email link

Future<Either<EmailLinkFailure, Unit>> signInWithEmailLink(Uri link)

EmailLinkFailure is also a custom data type that can hold failure cases we can imagine of when sign in with the link fails:

linkError
isNotSignInWithEmailLink
emailNotSet
signInFailed
userAlreadySignedIn

For the failure cases you don’t need to think much upfront just define the simple ones you can think of and the rest will come up as you develop. here I have defined all of them that I have faced during development.

Now we have defined our API interfaces which can convey business requirements, let's start implementing them with Firebase.

Configure Firebase with Flutter app

Please follow below link which has clearly explained all steps for Firebase integration and this tutorial becomes shorter. you should create a new Firebase project and integrate it to your cloned Flutter app.

Use Firebase official tutorial to add Firebase to your Flutter app

Before we continue read below guide on how authentication with firebase link works and also enable email link sign-in for your project which also explained in below:

Authentication with Firebase using email link

Then enable Firebase dynamic links in your Firebase project console. here is official video from firebase team:

Enable firebase dynamic links

Firebase dynamic links also requires to add SHA-256 fingerprint of your android app to your firebase project. navigate to android folder of the project in terminal and type ./gradlew signingReport . then copy the SHA-256 fingerprint to your android project in Firebase console:

SHA finger print used by Firebase as a security measure to allow that only your app can handle the sign-in link.

Now that you have clear understanding of how Firebase passwordless authentication works and have enabled passwordless sign-in and dynamic links for our Firebase project, we are ready to implement our business requirements.

Implement Passwordless sign-in

We develop step by step and I explain as we go forward. Lets add required dependencies in pubspec.yaml:

dependencies:
firebase_auth: ^4.6.0
firebase_core: ^2.12.0
firebase_dynamic_links: ^5.3.0
flutter:
sdk: flutter
flutter_secure_storage: ^8.0.0
fpdart: ^0.6.0
freezed_annotation: ^2.2.0

dev_dependencies:
build_runner: ^2.4.2
flutter_test:
sdk: flutter
freezed: ^2.3.3
lint: ^2.1.2

Create a folder named auth and add below file:

email_secure_store.dart

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class EmailSecureStore {
static const String _storageUserEmailAddressKey = 'userEmailAddress';
final FlutterSecureStorage _secureStorage;

const EmailSecureStore(this._secureStorage);

Future<String?> getEmail() =>
_secureStorage.read(key: _storageUserEmailAddressKey);

Future<void> setEmail(String email) => _secureStorage.write(
key: _storageUserEmailAddressKey,
value: email,
);
}

The EmailSecureStore has only one responsibility to just read or write the user email in a secure way on the device. Its part of Firebase requirements to verify that the sign-in link used on the exact device that has requested sign-in link.

Create a folder named models in theauth folder and add below files:

failure.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'failure.freezed.dart';

class Failure {
final String? message;
Failure.unexpectedError([this.message]);
}

@freezed
class EmailLinkFailure with _$EmailLinkFailure {
const factory EmailLinkFailure.linkError([String? message]) = _LinkError;
const factory EmailLinkFailure.isNotSignInWithEmailLink() =
_IsNotSignInWithEmailLink;
const factory EmailLinkFailure.emailNotSet() = _EmailNotSet;
const factory EmailLinkFailure.signInFailed([String? message]) =
_SignInFailed;
const factory EmailLinkFailure.userAlreadySignedIn() = _UserAlreadySignedIn;
}

EmailLinkFailure union cases defined using freezed package, which is a handy code generator to define union types in Dart. although Dart 3.0 had introduced sealed classes and pattern matching, but using union cases generated by freezed package is much handier. freezed package is a dev dependency and just generated codes for you. it means you can remove freezed package dependency from your code base, and your code still compiles.

Run code generator to generate freezed file with needed codes:

flutter pub run build_runner build --delete-conflicting-outputs

Next add user.dart

class User {
final String id;
factory User(String id) {
return User._(id);
}
User._(this.id);
}

The user model is so simple and just have an id.

sign_in_link_settings.dart

class SignInLinkSettings {
final String url;
final String androidPackageName;
final String iOSBundleId;
final String dynamicLinkDomain;

SignInLinkSettings({
required this.url,
required this.androidPackageName,
required this.iOSBundleId,
required this.dynamicLinkDomain,
});
}

SignInLinkSettings will be used by Firebase to be instructed how to construct the email link.

Until now our models are defined now create a file named passwordless_authenticator.dart in the auth folder and add below content:

import 'dart:async';

import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:fpdart/fpdart.dart';
import 'package:passwordless_signin/auth/email_secure_store.dart';
import 'package:passwordless_signin/auth/models/failure.dart';
import 'package:passwordless_signin/auth/models/sign_in_link_settings.dart';

class PasswordlessAuthenticator {
final firebase_auth.FirebaseAuth _firebaseAuth;
final EmailSecureStore _emailSecureStore;
final SignInLinkSettings _signinLinkSettings;

PasswordlessAuthenticator(
this._firebaseAuth,
this._emailSecureStore,
this._signinLinkSettings,
);

// 1
Future<Either<Failure, Unit>> sendSignInLinkToEmail(
String email,
) async {
try {
// 2
final actionCodeSttings = firebase_auth.ActionCodeSettings(
url: _signinLinkSettings.url,
handleCodeInApp: true,
androidInstallApp: true,
androidPackageName: _signinLinkSettings.androidPackageName,
iOSBundleId: _signinLinkSettings.iOSBundleId,
dynamicLinkDomain: _signinLinkSettings.dynamicLinkDomain,
);
// 3
await _emailSecureStore.setEmail(email);
// 4
await _firebaseAuth.sendSignInLinkToEmail(
email: email,
actionCodeSettings: actionCodeSttings,
);
// 5
return right(unit);
} on firebase_auth.FirebaseAuthException catch (e) {
// 6
return left(Failure.unexpectedError(e.message));
} catch (e) {
// 7
return left(Failure.unexpectedError());
}
}
}
  1. Starting to implement the first interface sendSignInLinkToEmail
  2. Creating a Firebase ActionCodeSettings based on passed SigninLinkSettings . In short we are telling firebase how to create sign-in link and how it should be handled when user opens the link. for more info I recommend read the official doc.
  3. Saving the email, will be used later to check if user has requested sign-in link on the same device. its required by Firebase.
  4. Request sending the sign-in link
  5. on success we just return an empty success response.
  6. 7. we catch any exceptions as a failure response.

Next we implement the second interface signInWithEmailLink:

Future<Either<EmailLinkFailure, Unit>> signInWithEmailLink(Uri link) async {
Either<EmailLinkFailure, Unit> result;
try {
// 1
final user = _firebaseAuth.currentUser;
if (user != null) {
result = left(const EmailLinkFailure.userAlreadySignedIn());
}
// 2
final email = await _emailSecureStore.getEmail();
if (email == null) {
result = left(const EmailLinkFailure.emailNotSet());
}
// 3
else if (_firebaseAuth.isSignInWithEmailLink(link.toString())) {
// 4
await _firebaseAuth.signInWithEmailLink(
email: email,
emailLink: link.toString(),
);
result = right(unit);
} else {
// 5
result = left(const EmailLinkFailure.isNotSignInWithEmailLink());
}
} on firebase_auth.FirebaseAuthException catch (e) {
// 6
result = left(EmailLinkFailure.signInFailed(e.message));
} catch (e) {
// 7
result = left(EmailLinkFailure.signInFailed(e.toString()));
}
return result;
}
  1. Check that user is not signed in.
  2. Check that email is set when sign-in link requested.
  3. Check the link is a sign-in link.
  4. Sign-in with the link.
  5. 6. 7. Convert exceptions to proper failure cases.

We have implemented the initial APIs. we try to use them get the new user signed-in. we are developing based on our initial understanding of the requirements and in small steps. as we are going forward we will find out how we can use these APIs or other needed methods.

Using the API

User should be able to interact with our API. so let’s connect the email field to the API so user can submit email to receive sign-in link. we use BLoC pattern to connect UI to our business logic. This pattern is easy to understand and read thanks to flutter_bloc library. Add flutter_bloc package to pubspec.yaml dependencies section:

flutter_bloc: ^8.1.2

If you are using VSCode, I suggest install bloc extension. it has provides handy tools to create BLoC templates.

Create a new bloc named passwordless_signin_bloc by right clicking on passwordless_signin folder and selecting new bloc. the extension will generate the templated files.

UI events:

Let’s list user interactions with UI:

  1. User enters email.
  2. User taps on Sign in button to request Sign in link.
  3. User taps on Open mail app to check for incoming emailed link.

We convert them to UI events

passwordless_signin_event.dart

part of 'passwordless_signin_bloc.dart';

@freezed
class PasswordlessSigninEvent with _$PasswordlessSigninEvent {
const factory PasswordlessSigninEvent.emailChanged(String email) =
_EmailChanged;
const factory PasswordlessSigninEvent.sendMagicLink() = _SendMagicLink;
const factory PasswordlessSigninEvent.openMailApp() = _OpenMailApp;
}

Now we have defined all the events that come from the UI using freezed library.

UI state:

The Sign-in flow UI state is simple, it should contain:

  1. Email field validation result
  2. Is submitting the form (we can display a loading indicator based on that)
  3. Submission result (failure or success)

UI will reflect to this state.

passwordless_signin_state.dart

part of 'passwordless_signin_bloc.dart';

typedef FailureMessage = String;

@freezed
class PasswordlessSigninState with _$PasswordlessSigninState {
const factory PasswordlessSigninState({
required Option<Either<FailureMessage, String>> emailAddress,
required bool isSubmitting,
required Option<Either<FailureMessage, Unit>> failureOrSuccessOption,
}) = _PasswordlessSignInFormState;

factory PasswordlessSigninState.initial() => PasswordlessSigninState(
emailAddress: none(),
isSubmitting: false,
failureOrSuccessOption: none(),
);
}

The state has been modeled with freezed package as it helps you to focus on model requirements instead of taking time to write a lot of boilerplate code. You may have noticed Option keyword. Option is also another functional programming data type corresponding to null in object oriented programming languages which can contain some data or none. Option like either forces us to handle both side of if/else cases. means less bugs.

The email field validation should be according to below requirments:

  1. by tapping on Sign in button, entered email should be validated.
  2. In case of invalid email, a validation error should be displayed underneath the field.
  3. When user is typing, validation error should be cleared.
  4. When email is valid and user taps on Sign in button, user should navigate to the next page.
email field validation

The Business Logic Component

Now lets complete the passwordless_signin_bloc based on the above requirements:

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fpdart/fpdart.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:passwordless_signin/auth/passwordless_authenticator.dart';

part 'passwordless_signin_bloc.freezed.dart';
part 'passwordless_signin_event.dart';
part 'passwordless_signin_state.dart';

class PasswordlessSigninBloc
extends Bloc<PasswordlessSigninEvent, PasswordlessSigninState> {
final PasswordlessAuthenticator _authenticator;

PasswordlessSigninBloc(this._authenticator)
: super(PasswordlessSigninState.initial()) {
on<PasswordlessSigninEvent>((event, emit) async {
// 1
await event.map(
emailChanged: (event) async => _emailChanged(event, emit),
sendMagicLink: (event) async => _sendMagicLink(event, emit),
openMailApp: (event) async => _openMailApp(event, emit),
);
});
}

void _emailChanged(
_EmailChanged event,
Emitter<PasswordlessSigninState> emit,
) {
// 2
emit(
state.copyWith(
emailAddress: some(right(event.email)),
failureOrSuccessOption: none(),
),
);
}

Future<void> _sendMagicLink(
_SendMagicLink event,
Emitter<PasswordlessSigninState> emit,
) async {}

void _openMailApp(
_OpenMailApp event,
Emitter<PasswordlessSigninState> emit,
) {}
}
  1. All UI events are mapped to corresponding methods.
  2. When user changes email, emits new state with updated email address. email will not be validated here.

Add below code to the _sendMagicLink method:

  Future<void> _sendMagicLink(
_SendMagicLink event,
Emitter<PasswordlessSigninState> emit,
) async {
// 1
final emailValidationResult =
_validateEmailAddress(state.emailAddress.value ?? '');
await emailValidationResult.fold(
(invalidEmail) async =>
//2
emit(
state.copyWith(
emailAddress: some(emailValidationResult),
isSubmitting: false,
failureOrSuccessOption: none(),
),
),
(validEmail) async {
// 3
emit(
state.copyWith(isSubmitting: true, failureOrSuccessOption: none()),
);
// 4
final failureOrSuccess = await _authenticator.sendSignInLinkToEmail(
validEmail,
);
// 5
emit(
state.copyWith(
isSubmitting: false,
failureOrSuccessOption: some(
failureOrSuccess.fold(
(l) => left(l.message ?? 'Unknown error'),
(r) => right(r),
),
),
),
);
},
);
}

Either<FailureMessage, String> _validateEmailAddress(String email) {
const emailRegex =
r"""^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+""";
if (RegExp(emailRegex).hasMatch(email)) {
return right(email);
} else {
return left('Enter valid email');
}
}

And add below extension to the end of passwordless_signin_bloc file:

// 6
extension EitherValueOrFailure on Option<Either<FailureMessage, String>> {
String? get value => fold(() => null, (r) => r.fold((l) => null, (r) => r));
String? get validationError => fold(
() => null,
(a) => a.fold((validationError) => validationError, (r) => null),
);
}
  1. Validates email address
  2. If email is invalid, emits new state with validation result. so the UI has to reflect the new state.
  3. If email is valid, emit new state of loading, so UI should display a loading indicator
  4. Request authenticator to send a sign in link to the user email.
  5. And after receiving the result, we emit new state.
  6. Its an extension on Option<Either<FailureMessage, String>> to get the value it holds. first we fold the option, if it contains a value (Either<FailureMessage, String>) then we fold it again and if has a String value we return it back, otherwise a null value will be returned. The diagram below show how we traverse to get the value or validation error:
Extracting Value from Option and Either

Connecting UI to the BLoC

Open EmailPage widget and wrap theListView with a BlocConsumer of PasswordlessSigninBloc.

BlocConsumer<PasswordlessSigninBloc, PasswordlessSigninState>(
listener: (context, state) {
state.failureOrSuccessOption.fold(
() => null,
(a) => a.fold(
(message) => showGenericDialog(
context: context,
title: 'Error',
message: message,
optionsBuilder: () => {'OK': true},
),
(r) => context.goNamed(Routes.emailSent.name),
),
);
},
builder: (context, state) {
// Omitted the rest for the clarity
},)

In the listener code block, in case of failing to send the sign-in link we display an error alert, and on the success case, we navigate to theemaiSent page.

Add below lines to the email TextField and PrimaryButton:

TextField(
decoration: InputDecoration(
...
errorText: state.emailAddress.validationError,
...

),
...
onChanged: (value) => context
.read<PasswordlessSigninBloc>()
.add(PasswordlessSigninEvent.emailChanged(value)),
),
...
PrimaryButton(
...
onPressed: () {
context
.read<PasswordlessSigninBloc>()
.add(const PasswordlessSigninEvent.sendMagicLink());
},
),

Whenever state gets updated, the builder function gets called. so if there is validation error, text field will display it. and when text field value gets changed or primary button gets pressed, we send proper event to the bloc.

But we need to create and pass the PasswordlessSigninBloc to the BlocConsumer . let me show you the dependency graph looks like:

Dependency Graph of PassworlessSigninBloc

We can’t simply instantiate PasswordlessSigninBloc in the EmailPage, as it requires other dependencies to be instantiated first before being able to create PasswordlessSigninBloc which forces EmailPage to know a lot of unnecessary things and becomes hard to maintain. In order to resolve that we us get_it as a Service Locator to register and locate the dependencies.

Add below dependencies to the pubspec.yaml:

  get_it: ^7.4.1
package_info_plus: ^4.0.0

package_info_plus is used to get package name or bundle Id. we need it when we are constructing SigninLinkSettings .

Create a file named injection.dart in the lib folder and paste the below content:

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_it/get_it.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:passwordless_signin/auth/email_secure_store.dart';
import 'package:passwordless_signin/auth/models/sign_in_link_settings.dart';
import 'package:passwordless_signin/auth/passwordless_authenticator.dart';

final GetIt getIt = GetIt.instance;

Future<void> configureInjection() async {
await Firebase.initializeApp();

final packageInfo = await PackageInfo.fromPlatform();
const emailStore = EmailSecureStore(FlutterSecureStorage());

final authenticator = PasswordlessAuthenticator(
FirebaseAuth.instance,
emailStore,
SignInLinkSettings(
url: 'https://yourWebsite.com',
androidPackageName: packageInfo.packageName,
iOSBundleId: packageInfo.packageName,
dynamicLinkDomain: 'yousubdomain.page.link',
),
);

// Register PasswordlessAuthenticator
getIt.registerLazySingleton<PasswordlessAuthenticator>(
() => authenticator,
);
}

The configureInjection is straightForward. just instantiating dependencies and passing them to the PasswordlessAuthenticator and finally registering the PasswordlessAuthenticator as a lazy singleton. meaning when needed the object will be instantiated and only one instance of it will be available during app lifetime. The url in SignInLinkSettings is a fallback url when user opens the link on the desktop. dynamicLinkDomain is the link you have defined in the Dynamic Links of firebase console.

Dynamic Link

Now open main.dart and call configureInjection before runApp function.

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureInjection();

runApp(
const MyApp(),
);
}

Open email_page.dart and wrapBlocConsumer with the BlocProvider and pass new PasswordlessSigninBloc below code to the create property:

BlocProvider(
create: (_) => PasswordlessSigninBloc(getIt<PasswordlessAuthenticator>()),
child: BlocConsumer<PasswordlessSigninBloc, PasswordlessSigninState>(
....

You did great work until now, we connected a lot of dots together. lets run the app and see how it works:

Email field is connected to the UI

Perfect 👏👏👏

Lets see the error alert also, temporarily replace contents of sendMagicLink with:

emit(state.copyWith(isSubmitting: true, failureOrSuccessOption: none()),);

await Future.delayed(const Duration(seconds: 1));

emit(
state.copyWith(
isSubmitting: false,
failureOrSuccessOption: some(
left('This is a sample error'),
),
),
);
Error alert

We also manually tested how UI looks like in failure case. we will automate this test in the later parts.

Now we need connect the Open email app button to the BLoC. update pubspec.yaml with new dependencies:

dependencies:
android_intent_plus: ^4.0.0
url_launcher: ^6.1.11

Open passwordless_signin_bloc.dart and replace _openMailApp with below code:

import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter/foundation.dart';
import 'package:url_launcher/url_launcher.dart';

....

void _openMailApp(
_OpenMailApp event,
Emitter<PasswordlessSigninState> emit,
) {
if (defaultTargetPlatform == TargetPlatform.iOS) {
launchUrl(Uri.parse('message://'));
} else if (defaultTargetPlatform == TargetPlatform.android) {
const AndroidIntent intent = AndroidIntent(
action: 'android.intent.action.MAIN',
category: 'android.intent.category.APP_EMAIL',
);
intent.launch();
}
}

We only want to open email app without email composition so:

  1. In iOS we call message URL scheme.
  2. On android we use intent to only launch mail activity.

Open email_sent_page.dart and update onPressed property of PrimaryButton:

PrimaryButton(
title: 'Open email app',
onPressed: () => context
.read<PasswordlessSigninBloc>()
.add(const PasswordlessSigninEvent.openMailApp()),
),

Now try to navigate to EmailSentPage and tap on Open email app button, but you face below error 💥:

Error: Could not find the correct Provider<PasswordlessSigninBloc> above this EmailSentPage Widget 

It says it can’t find PasswordlessSigninBloc in the widget tree. as it was created in EmailPage. we need to find a way that PasswordlessSigninBloc is accessible for both pages.

Before

You may think we should register PasswordlessSigninBloc as a singleton via get_it and locate it in both pages. it may work initially and even for this simple scenario, but it gradually leads to disaster as your app scales.PasswordlessSigninBloc should be automatically wiped from memory when sign-in flow finished. get_it can’t automatically dispose the object when its not needed. You may even think you will register in a BlocProvider on top of the main app, but it will still remain on the memory as long as app is active! but then what should we do?

GoRouter is to the rescue 🛟

We are using go_router that uses Flutter Router API (Navigator 2) to navigate between different pages of the app. It gives us flexibility to have hierarchal navigation instead of having linear one.

GoRouter has a route named ShellRoute that wraps child routes. means we can use different independent navigators for matching sub routes. ShellRoute has a builder property with achild parameter. The child parameter is the matching sub route and we can wrap it with BlocProviderso sub routes can have retrieve PasswordlessSigninBloc via the BuildContext

Open routes.dart file and wrap emailForm sub route with a shellRoute:

ShellRoute(
builder: (context, state, child) {
return BlocProvider(
create: (context) =>
PasswordlessSigninBloc(getIt<PasswordlessAuthenticator>()),
child: child,
);
},
routes: [
GoRoute(
name: Routes.emailForm.name,
path: Routes.emailForm.path,
builder: (context, state) => const EmailPage(),
routes: [
GoRoute(
name: Routes.emailSent.name,
path: Routes.emailSent.path,
builder: (context, state) => const EmailSentPage(),
),
],
),
],
),
PasswordlessSigninBloc now is accessible by both pages

Now both EmailForm and EmailSent pages have access to the PasswordlessSigninBloc. Open email_page.dart file and remove redundant BlocProvider. Now tapping on the Open email app button, should work.

Its The Demo Time🤩

Lets review until now:

  1. We integrated firebase project with Flutter app.
  2. Enabled Passworldless sign-in in Firebase.
  3. Created a dynamic link.
  4. Instructed FirebaseAuth to send sign-in link by the settings provided.
  5. Validating email field.
  6. Calling sendSignInLink API via the BLoC pattern to send sign-in link.
  7. Enabled opening the email app by tapping on Open email app button.

And here it is:

But when the app opens nothing happened! We need to use the second APIsignInWithEmailLink(Uri link) to sign-in the user after launch. I have covered it in the second part of the tutorial, before reading the second part, have a tea and take a few minutes to rest. ☕️

You can view the code until here by switching to send-signin-link branch

git checkout send-signin-link

In the second part we will implement sign-in with the link and will complete the app:

Part 2: Sign-in the user

See you there 😉

--

--

Hashem Abounajmi
Hashem Abounajmi

No responses yet