OTP Autofill in Flutter UI
Hey friends. Today we’re going to discuss how can we fill up the OTP fields of our screen(Flutter UI) automatically as the OTP SMS comes.
Table of Contents :
OTP Autofilling — Introduction and Uses
OTP Autofill —step by step implementation
Introduction and Uses:-
These days we see several times that as we approach getting signed-in in many applications we need to verify our phone numbers. Although, we can do so in mainly two ways, either by making a phone call or by an OTP — sent as sms to our phone numbers.
So along with that, we have also noticed, that the OTP which comes with the sms got filled up automatically in the OTP fields which makes UI more user-friendly. Well, how does that happen? I’m going to answer the question with the help of this article along with my demo project.
In this session, we’d be using the most commonly used package for such purposes, that is sms_autofill.
Working with this package made it comparatively easier and faster to get my expected output. We just need to follow a few steps to make it work properly throughout our project which we’ll discuss later in this article.
To get the OTP sms, I’ve used Firebase Phone Authentication. To achieve that we first need to create a Firebase project in Firebase Console. Once you have created one, make sure that you enable the Phone sign-in method in the Authentication section.
Firebase Phone Authentication:-
Here’s how we do it:-
To know more, you may go to the Apple, Android, or web options according to the project environment in which you build the project. For me it was Android.
Save it as you enable it. It will look like this
Now firstly, we need to send an OTP sms to the entered mobile number.
Code Implementation:-
CommonUtils.dart
class CommonUtils {
static String verify = "";
static Future<String> firebasePhoneAuth(
{required String phone, required BuildContext context}) async {
try {
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: phone,
verificationCompleted: (PhoneAuthCredential credential) {
print("Phone credentials $credential");
},
verificationFailed: (FirebaseAuthException e) {
print("Verification failed $e");
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Verification Failed! Try after some time.")));
},
codeSent: (String verificationId, int? resendToken) async {
CommonUtils.verify = verificationId;
print("Verify: $verificationId");
},
codeAutoRetrievalTimeout: (String verificationId) {},
);
return CommonUtils.verify;
} catch (e) {
print("Exception $e");
return "";
}
}
}
Here, we’ll pass the phone number (fetched by the TextEditingController) to the phone argument of this function.
We used Future<String> as the return type because we are going to return the verification ID that we get during the process of sending OTP. Further, this verification ID will be needed in the OTP screen.
PhoneAuthCredential credential =
PhoneAuthProvider.credential(
verificationId: CommonUtils.verify,
smsCode: otpCode);
await auth.signInWithCredential(credential);
setState(() {
isLoaded = false;
});
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => const HomeScreen()));
}
This is a snippet from the OtpScreen.dart where we are using the verification ID we extracted from CommonUtils.firebasePhoneAuth() method. Along with that, we pass our smsCode received. It then matches OTP which was sent and the OTP which is being passed in smsCode along with the verification ID. If they match, we successfully will be authorized to get through the other page (HomeScreen.dart).
OTP Autofill:-
Now let’s get through the steps to implement otp-auto-filling
Import the latest version of the sms_autofill package in pubspec.yaml file
dependencies:
sms_autofill: ^2.2.0
Go to >android >app >build.gradle file and change minSdkVersion from 16 to 19
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.otp_autofill_demo"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 19
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Add listner listenForCode() method inside initState() method to listen otp sms as soon as possible.
@override
void initState() {
_listenOtp();
super.initState();
}
void _listenOtp() async {
await SmsAutoFill().listenForCode();
print("OTP Listen is called");
}
Use the PinFieldAutoFill() widget as our OTP field. You can apply decoration to it to make it more attracting. I’ve used BoxLooseDecoration() constructor to decorate fields.
PinFieldAutoFill(
currentCode: otpCode,
decoration: const BoxLooseDecoration(
radius: Radius.circular(12),
strokeColorBuilder: FixedColorBuilder(
Color(0xFF8C4A52))),
codeLength: 6,
onCodeChanged: (code) {
print("OnCodeChanged : $code");
otpCode = code.toString();
},
onCodeSubmitted: (val) {
print("OnCodeSubmitted : $val");
},
)
Override dispose() method to unregister stream listener. This is a very significant step as the stream should be closed once we’re done using it. Otherwise, we could face memory leakage problem.
@override
void dispose() {
SmsAutoFill().unregisterListener();
print("Unregistered Listener");
super.dispose();
}
Well, by this we’ve completed all the steps we needed to do. Further will be the whole code implementation of the demo project.
Code File:-
main. dart
void main() async{
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const LoginScreen(),
);
}
}
LoginScreen.dart
class LoginScreen extends StatefulWidget {
static String verify = "";
const LoginScreen({Key? key}) : super(key: key);
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _phoneController = TextEditingController();
final _countryCodeController = TextEditingController();
Color focusBorderColor = Colors.black12;
Color underlineColor = Colors.grey.shade200;
RegExp regExp = RegExp(r'^[0]?[6789]\d{9}$');
RegExp countryRegExp = RegExp(r'^\+?(\d+)');
bool divider = false;
bool isLoaded = false;
int validatePhone(String phone) {
String pattern = r'^[0]?[6789]\d{9}$';
RegExp regExp = RegExp(pattern);
if (phone.isEmpty) {
return 1;
} else if (phone.length < 10) {
return 2;
} else if (!regExp.hasMatch(phone)) {
return 3;
} else {
return 0;
}
}
@override
Widget build(BuildContext context) {
return ColorfulSafeArea(
color: const Color(0xFF8C4A52),
child: SafeArea(
child: Scaffold(
backgroundColor: isLoaded ? Colors.white : const Color(0xFF8C4A52),
body: isLoaded
? const Center(child: CircularProgressIndicator())
: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Column(
children: [
Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(top: 50),
child: Container(
height: 50,
)),
const SizedBox(
height: 25,
),
WhiteContainer(
headerText: "Login",
labelText:
"Please enter your 10 digit phone no to proceed",
child: BorderBox(
margin: false,
padding: const EdgeInsets.symmetric(
horizontal: 15),
color: Colors.grey.shade200,
height: 60,
child: Form(
key: _formKey,
child: Row(
children: [
BorderBox(
height: 100,
width: 55,
padding:
const EdgeInsets.symmetric(
vertical: 2,
horizontal: 8),
color: Colors.grey.shade200,
margin: false,
child: TextFormField(
controller:
_countryCodeController,
textInputAction:
TextInputAction.next,
onChanged: (value) {
if (!countryRegExp
.hasMatch(value)) {
setState(() {
focusBorderColor =
Colors.red.shade900;
underlineColor =
focusBorderColor;
});
} else {
setState(() {
focusBorderColor =
Colors.green.shade700;
underlineColor =
focusBorderColor;
});
}
if (value.length == 4) {
FocusScope.of(context)
.nextFocus();
}
},
validator: (value) {
if (value!.isEmpty) {
return "Code?";
} else if (!countryRegExp
.hasMatch(value)) {
return "Invalid";
}
},
style: TextStyle(fontSize: 19),
keyboardType:
TextInputType.phone,
inputFormatters: [
LengthLimitingTextInputFormatter(
4)
],
decoration: InputDecoration(
hintText: "+91",
hintStyle: TextStyle(
color:
Colors.grey.shade400),
enabledBorder:
UnderlineInputBorder(
borderSide: BorderSide(
color: underlineColor),
),
focusedBorder:
UnderlineInputBorder(
borderSide: BorderSide(
color:
focusBorderColor),
),
),
)),
const Padding(
padding: EdgeInsets.symmetric(
vertical: 7.0),
child: VerticalDivider(
thickness: 2,
),
),
BorderBox(
height: 100,
width: 250,
color: Colors.grey.shade200,
padding:
const EdgeInsets.symmetric(
vertical: 2,
horizontal: 8),
margin: false,
child: TextFormField(
controller: _phoneController,
validator: (value) {
int res =
validatePhone(value!);
if (res == 1) {
return "Please enter number";
} else if (res == 2) {
return "Please enter 10 digits phone number";
} else if (res == 3) {
return "Please enter a valid 10 digits phone number";
} else {
return null;
}
},
style: const TextStyle(
fontSize: 19),
keyboardType:
TextInputType.phone,
onChanged: (value) {
if (value.isEmpty) {
FocusScope.of(context)
.previousFocus();
} else if (value.length ==
10) {
FocusScope.of(context)
.unfocus();
}
},
inputFormatters: [
LengthLimitingTextInputFormatter(
10),
FilteringTextInputFormatter
.digitsOnly
],
decoration: InputDecoration(
border: InputBorder.none,
hintText: "XXXXXXXXXX",
hintStyle: TextStyle(
color: Colors
.grey.shade400)),
)),
],
),
))),
],
),
),
],
),
bottomNavigationBar: Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
color: Colors.white,
child: GestureDetector(
onTap: () async {
setState(() {
isLoaded = true;
});
if (_formKey.currentState!.validate()) {
var appSignature = await SmsAutoFill().getAppSignature;
LoginScreen.verify = await CommonUtils.firebasePhoneAuth(
phone: _countryCodeController.text +
_phoneController.text,
context: context);
Future.delayed(const Duration(seconds: 5)).whenComplete(() {
setState(() {
isLoaded = false;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => OtpScreen(
phone: _countryCodeController.text +
_phoneController.text)));
print("App Signature : $appSignature");
});
});
}
},
child: const BorderBox(
margin: false,
color: Color(0xFF8C4A52),
height: 50,
child: Text(
"Proceed",
style: TextStyle(fontSize: 18, color: Colors.white),
),
)),
),
),
));
}
}
OtpScreen.dart
class OtpScreen extends StatefulWidget {
final String phone;
const OtpScreen({Key? key, required this.phone}) : super(key: key);
@override
State<OtpScreen> createState() => _OtpScreenState();
}
class _OtpScreenState extends State<OtpScreen> {
String otpCode = "";
String otp = "";
bool isLoaded = false;
final _formKey = GlobalKey<FormState>();
final FirebaseAuth auth = FirebaseAuth.instance;
@override
void initState() {
_listenOtp();
super.initState();
}
@override
void dispose() {
SmsAutoFill().unregisterListener();
print("Unregistered Listener");
super.dispose();
}
void _listenOtp() async {
await SmsAutoFill().listenForCode();
print("OTP Listen is called");
}
@override
Widget build(BuildContext context) {
return ColorfulSafeArea(
color: const Color(0xFF8C4A52),
child: SafeArea(
child: Scaffold(
backgroundColor: isLoaded ? Colors.white : const Color(0xFF8C4A52),
body: isLoaded
? const Center(child: CircularProgressIndicator())
: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Column(
children: [
Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(top: 50),
child: Container(
height: 50,
)),
const SizedBox(
height: 25,
),
WhiteContainer(
headerText: "Enter OTP",
labelText:
"OTP has been successfully sent to your \n ${widget.phone}",
child: Container(
height: 70,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
PinFieldAutoFill(
currentCode: otpCode,
decoration: const BoxLooseDecoration(
radius: Radius.circular(12),
strokeColorBuilder: FixedColorBuilder(
Color(0xFF8C4A52))),
codeLength: 6,
onCodeChanged: (code) {
print("OnCodeChanged : $code");
otpCode = code.toString();
},
onCodeSubmitted: (val) {
print("OnCodeSubmitted : $val");
},
)
],
),
),
),
],
),
),
],
),
bottomNavigationBar: Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 30),
color: Colors.white,
child: GestureDetector(
onTap: () async {
print("OTP: $otpCode");
setState(() {
isLoaded = true;
});
if (_formKey.currentState!.validate()) {
try {
PhoneAuthCredential credential =
PhoneAuthProvider.credential(
verificationId: CommonUtils.verify,
smsCode: otpCode);
await auth.signInWithCredential(credential);
setState(() {
isLoaded = false;
});
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => const HomeScreen()));
} catch (e) {
setState(() {
isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Wrong OTP! Please enter again")));
print("Wrong OTP");
}
}
},
child: const BorderBox(
margin: false,
color: Color(0xFF8C4A52),
height: 50,
child: Text(
"Continue",
style: TextStyle(fontSize: 18, color: Colors.white),
),
),
),
),
)));
}
}
You can build your home page as you like.
As we run the application, this is what we get as our final output.
Conclusion:-
In this article, we’ve learned how to autofill OTP from OTP sms. Moreover, we also got to know about Firebase Phone Authentication and implemented it in the project.
❤ ❤ Thanks for reading this article ❤❤
If I got something wrong? Let me know in the comments. I would love to improve.
Clap 👏 If this article helps you.
Github Link:
Find the source code of the OTP Autofill :
GitHub – flutter-devs/OTP_Autofill
You can’t perform that action at this time. You signed in with another tab or window. You signed out in another tab or…github.com
Feel free to connect with us:
And read more articles from FlutterDevs.com.
FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire a flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on Facebook, GitHub, Twitter, and LinkedIn.
We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.