tru.ID logo
LoginSignup

Passwordless Mobile Authentication with Flutter

In this tutorial, you'll learn how to add passwordless authentication to a Flutter application signup onboarding flow, using PhoneCheck to verify the phone number associated with the SIM card on a mobile device.

Timothy Ogbemudia

Developer Experience Engineer

Last updated: 14 October 2021

tutorial cover image

Up to 52% of users who walk away from using an app within the first 90 days do so due to poor customer onboarding. The signup flow is a critical part of the onboarding flow. 20-30% of signup attempts fail on mobile due to incomplete authentication, often using legacy options – an SMS is delayed or not delivered at all, an email ends up in the spam folder or is undelivered.

This problem is where the tru.ID PhoneCheck API comes in. It confirms the ownership of a mobile phone number by verifying the possession of an active SIM card with the same number.

A mobile data session gets created to a unique check URL for this verification. tru.ID then resolves a match between the verification of a phone number and the phone number that the mobile network operator (MNO) identifies as the owner of the mobile data session.

If you want to dive into the finished code, you can find it on GitHub.

Before you begin

To follow along with this tutorial, you'll need:

  • A tru.ID Account
  • A mobile phone with an active data SIM card

Getting started

In your terminal, clone the starter-files branch with the following command:

$ git clone -b starter-files --single-branch https://github.com/tru-ID/passwordless-auth-flutter.git

You'll also need the tru.ID CLI, which is used to create a project and run a local server for development. Install this CLI in your terminal with the following command:

$ npm i -g @tru_id/cli

Enter your tru.ID credentials, which can be found within the tru.ID console.

Install the tru.ID CLI development server plugin.

Create a new tru.ID project within the root directory via:

$ tru projects:create passwordless-auth-flutter --project-dir .

Run the development server, pointing it to the directory containing the newly created project configuration. This command will also open up a local tunnel to your development server, making it publicly accessible to the Internet so that your mobile phone can access it when only connected to mobile data. Please note the LocalTunnel URL, as you will need to use it later in the tutorial.

$ tru server -t

To start the project, ensure you have a physical device connected, run the following commands to install all of the application's dependencies, and then run the application on your device:

$ flutter pub get
$ flutter run

Note To run this application on an iOS device, please ensure you've provisioned your project in XCode.

The example onboarding flow is shown in the three images below:

A tru.ID mobile app onboarding screen with a woman using her phone as the background and the text 'Mobile Authentication, Reimagined'
A tru.ID mobile app onboarding screen with a person using their phone as the background and the text 'Invisible, Effortless, Feels like magic!'
A tru.ID mobile app screen with the tru.ID logo, a phone number text input and a submit button.

Get the user's phone number

The first step is to get the user's phone number. Before this happens, update the baseURL value in lib/registration.dart to the value of the LocalTunnel URL you noted earlier in the tutorial.

final String baseURL = 'https://neat-fish-91.loca.lt';

Next, you need to add pieces of state to store the phone number and a piece of state, loading, which helps trigger a visual cue that something (an HTTP request) is happening.

Find the line class _RegistrationState extends State<Registration>, and below this line add the following two to define phoneNumber and loading as variables:

String? phoneNumber;
bool loading = false;

The application needs to update the phoneNumber variable when the user has entered or altered theirs. So find the line: onChanged: (text) {}, and update it to the following example:

onChanged: (text) {
setState(() {
phoneNumber = text;
});
},

A visual indicator is needed to disable the button and display a loading image when the registration has been submitted. Near the bottom find the line: onPressed: () async {}, child: const Text('Register')), and update it as follows:

child: TextButton(
onPressed: () async {},
child: loading
? const CircularProgressIndicator()
: const Text('Register'),
),

Here, if loading is true, a CircularProgressIndicator widget acts as the visual cue.

Create reusable functions

You now need to create reusable functions for handling two possible outcomes.

The first possible outcome is an errorHandler function, which takes in the current BuildContext, title and content and returns an AlertDialog widget.

The second possible outcome is the successHandler function which returns a generic AlertDialog.

Above class _RegistrationState extends State<Registration>, add the following snippet of code which contains the two functions for these possible outcomes:

Future<void> errorHandler(BuildContext context, String title, String content) {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(title),
content: Text(content),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'Cancel'),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
);
});
}
Future<void> successHandler(BuildContext context) {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Registration Successful.'),
content: const Text('✅'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'Cancel'),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
);
});
}

Create the PhoneCheck

This next section makes a request to the reachability API, making sure the user's mobile network operator supports the PhoneCheck API. If so, it makes a PhoneCheck request. The tru.ID Flutter SDK contains all of the functionality to provide this experience. Install this SDK in the terminal with the following command:

$ flutter pub add tru_sdk_flutter
#or
$ dart pub add tru_sdk_flutter

Add the import tru.ID SDK and dart's convert library to the top of lib/registration.dart:

import 'package:tru_sdk_flutter/tru_sdk_flutter.dart';
import 'dart:convert';

In the same piece of code you've recently updated (the TextButton component), find the line: onPressed: () async {}, and replace it with the following:

setState(() {
loading = true;
});
TruSdkFlutter sdk = TruSdkFlutter();
String? reachabilityInfo = await sdk.isReachable();
ReachabilityDetails reachabilityDetails =
ReachabilityDetails.fromJson(
jsonDecode(reachabilityInfo!));
if (reachabilityDetails.error?.status == 400) {
return errorHandler(context, "Something Went Wrong.",
"Mobile Operator not supported.");
}
bool isPhoneCheckSupported = false;
if (reachabilityDetails.error?.status != 412) {
isPhoneCheckSupported = false;
for (var products in reachabilityDetails.products!) {
if (products.productName == "Phone Check") {
isPhoneCheckSupported = true;
}
}
} else {
isPhoneCheckSupported = true;
}
if (!isPhoneCheckSupported) {
return errorHandler(context, "Something went wrong.",
"PhoneCheck is not supported on MNO.");
}

In the code sample above the isReachable function gets called. This function returns the networkId, networkName, countryCode, and products. The products array is an optional array supported by the MNO. There is also an error object which contains any potential errors.

Next, the application creates the variable isPhoneCheckSupported. If the error status is not a 412, the application loops through the products and checks whether the productName equals Phone Check; if it does, then isPhoneCheckSupported is set to true.

If the isPhoneCheckSupported variable gets set to false by the end of the loop, the PhoneCheck is not supported. In this situation, errorHandler function is called. If isPhoneCheckSupported is true, the PhoneCheck can be created.

First, create a data class that represents the properties expected and convert the JSON response to a Dart Map.

Create a new file called models.dart in the lib directory and paste the following:

import 'dart:convert';
// function for converting from JSON to an object (or map)
PhoneCheck phoneCheckFromJSON(String jsonString) =>
PhoneCheck.fromJson(jsonDecode(jsonString));
class PhoneCheck {
String checkId;
String checkUrl;
PhoneCheck({required this.checkId, required this.checkUrl});
factory PhoneCheck.fromJson(Map<String, dynamic> json) => PhoneCheck(
checkId: json["check_id"],
checkUrl: json["check_url"],
);
}

The code you've just written creates a PhoneCheck class with two fields, the checkId and checkUrl, which represent the check_id and check_url values retrieved from the PhoneCheck API response.

A factory constructor is also created, which initializes final variables from a JSON object passed in.

It's now time to create a function that takes in a JSON string and passes in the decoded JSON object to the factory constructor. At the top of lib/registration.dart, add the import for your newly created models.dart:

import 'package:passwordless_auth_flutter/models.dart';

The next step is to create a function for creating the PhoneCheck resource. The HTTP package is needed to make HTTP network requests. In the terminal, run the following to install this package:

$ flutter pub add http
#or
$ dart pub add http

Import this new class at the top of lib.registration.dart:

import 'package:http/http.dart' as http;

Using this newly installed package, a function is needed to take the user's phone number and to make a POST request to the endpoint /phone-check. If the status is 200 -Ok, then call the helper method from the PhoneCheck class to convert the JSON to an object. Otherwise, return null.

Above the line Future<void> errorHandler(BuildContext context, String title, String content) {, create the following new function, createPhoneCheck:

Future<PhoneCheck?> createPhoneCheck(String phoneNumber) async {
final response = await http.post(Uri.parse('$baseURL/phone-check'),
body: {"phone_number": phoneNumber});
if (response.statusCode != 200) {
return null;
}
final String data = response.body;
return phoneCheckFromJSON(data);
}

The next step is to call the errorHandler function if there is an error. For this, in the onPressed property of the TextButton() widget, add the following:

onPressed: () async {
...
if (!isPhoneCheckSupported) {
return errorHandler(context, "Something went wrong.",
"PhoneCheck is not supported on MNO.");
}
final PhoneCheck? phoneCheckResponse =
await createPhoneCheck(phoneNumber!);
if (phoneCheckResponse == null) {
setState(() {
loading = false;
});
return errorHandler(context, 'Something went wrong.',
'Phone number not supported');
}
}

The image below is an example of what the user is presented with if the mobile network operator does not support their phone number.

A tru.ID mobile app screen with a modal overlayed with the title 'something went wrong' and a message saying 'phone number not supported'

Open the Check URL

When a PhoneCheck gets created, the response contains a check URL, which the application needs to make a GET request to over cellular for the check to be successful. The tru.ID Flutter SDK uses native functionality to force the network request through a cellular connection rather than WiFi. To make this request in the onPressed function within the TextButton widget, add the following code as shown below:

onPressed: () async {
...
// open check URL
String? result =
await sdk.check(phoneCheckResponse.checkUrl);
if (result == null) {
setState(() {
loading = false;
});
errorHandler(context, "Something went wrong.",
"Failed to open Check URL.");
}
}

The code above first opens the check URL; it then checks whether a result was received, which will indicate whether the check was successful or not.

Get the PhoneCheck result

The final implementation in this tutorial is to get the PhoneCheck result. To do this, a new data class needs creating, which will represent the properties expected in the PhoneCheck response.

At the bottom of lib/models.dart, add the following PhoneCheckResult class:

PhoneCheckResult phoneCheckResultFromJSON(String jsonString) =>
PhoneCheckResult.fromJson(jsonDecode(jsonString));
class PhoneCheckResult {
bool match;
PhoneCheckResult({required this.match});
factory PhoneCheckResult.fromJson(Map<String, dynamic> json) =>
PhoneCheckResult(match: json["match"]);
}

The application now needs to make a request to GET the PhoneCheck, passing in the check_id as a parameter. To make this request and use the newly created data class, open lib/registration.dart and underneath the createPhoneCheck function, add the following getPhoneCheck function:

Future<PhoneCheckResult?> getPhoneCheck(String checkId) async {
final response =
await http.get(Uri.parse('$baseURL/phone-check?check_id=$checkId'));
if (response.statusCode != 200) {
return null;
}
final String data = response.body;
return phoneCheckResultFromJSON(data);
}

Now, call the new function. Update onPressed in the TextButton widget to the following:

onPressed: () async {
...
final PhoneCheckResult? phoneCheckResult =
await getPhoneCheck(phoneCheckResponse.checkId);
if (phoneCheckResult == null) {
setState(() {
loading = false;
});
return errorHandler(context, 'Something Went Wrong.',
'Please contact support.');
}
if (phoneCheckResult.match) {
setState(() {
loading = false;
});
return successHandler(context);
} else {
setState(() {
loading = false;
});
return errorHandler(context, 'Registration Unsuccessful.',
'Please contact your network provider 🙁');
}
},

Here, you call the getPhoneCheck function, and if phoneCheckResult is null it calls the errorHandler. The application then checks whether the check has a match. If there is a match, then call the successHandler function.

The image below shows an example of the application if there is a match in the PhoneCheck.

A tru.ID mobile app screen with a modal overlayed with the title 'Registration Successful' and a message containing a green checkmark

Wrapping up

That's it! With everything in place, you now have a seamless signup onboarding flow with minimal UX friction, resulting in reduced user drop-offs.

Resources

tru.ID logo

Platform

Docs

DON'T MISS A BEAT — STAY ON THE DOT!

Keep current with industry news and updates from tru.ID.

Follow us on:

Made with ❤️ across the 🌍

© 2021 4Auth Limited. All rights reserved. tru.ID is the trading name of 4Auth Limited.