Panduka Nandara
Posted on December 31, 2020
In this article, I am going to explain how to add sign-in and signup process using AWS Cognito feature to your app using Amplify.
As you already know flutter is a cross-platform mobile development framework for both Android and iOS. Amplify is a set of AWS services gathered to build scalable full-stack applications. In the other hand, as Google has firebase, AWS has Amplify.
We will create a simple sign in and sign up application.
▶ Check out the Youtube video
👶 Initial Project Github
😎 Finished Project Github
To create the app you will need the following tools, 🔧
Flutter version 1.20.0
The latest version of Amplify CLI. You can install the latest Amplify CLI using the following command.
$ npm install -g @aws-amplify/cli@3.3.12
What we are going to do
1. Create a Flutter Project
You can simply create a flutter project using the following command. In this case, I will use the project name as amplify_login_app
.
$ flutter create todo_amplify
Then, to add amplify to the current project go project directory and open a new terminal. Then run the following command.
$ amplify init
Select the default options and continue.
After you initialized the Amplify, your project structure will be something like this.
As you can see there's an auto generated file calledamplifyconfiguration.dart
. This file is generated during initializing process. Do not rename or modify this file we will need this later.
Adding Authentication
Adding authentication to our app is pretty storage forward. Let's how it's done. First, run the following command in your current working directory.
$ amplify add auth
Note that we will not be able to edit these settings later
In our case, we select the sign-in method as Email. Depending on your requirement you can choose either username, phone number or email-based authentication.
Then we should run amplify push
to deploy the changes to the cloud and to update amplifyconfiguration.dart
.
Now let's do some coding stuff. We have to add AWS Cognito dependency to our project. We can do that by adding the dependencies amplify_core
and amplify_auth_cognito
to pubspec.yaml
.
-
amplify_core
is the core module for flutter amplify. You must always add this dependency if you use any amplify library. -
amplify_auth_cognito
is used to add the amplify auth feature to your app.
Then you can download all the dependencies using flutter pub get
.
If you are an android developer you have to update the minSdkVersion
version to 21. Because amplify_core
requires the minimum API level to be 21. To do that go to app-level build.gradle
file.
Let's integrate Amplify into our app.
As you can see in our main.dart
file, MyApp is the root widget in our app. I am going to remove all the widgets and change it as follows.
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
title: 'Amplify TODO',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: AmplifyTODO(),
),
);
}
class AmplifyTODO extends StatefulWidget {
AmplifyTODO({Key key}) : super(key: key);
@override
_AmplifyTODOState createState() => _AmplifyTODOState();
}
class _AmplifyTODOState extends State<AmplifyTODO> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
To initialize amplify we should run the following method inside initState
method.
import 'package:amplify_core/amplify_core.dart';
import 'package:amplify_todo/amplifyconfiguration.dart';
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
class _AmplifyTODOState extends State<AmplifyTODO> {
bool _amplifyConfigured = false;
Amplify _amplifyInstance = Amplify();
Future<void> _configureAmplify() async {
try {
AmplifyAuthCognito auth = AmplifyAuthCognito();
_amplifyInstance.addPlugin(
authPlugins: [auth],
);
await _amplifyInstance.configure(amplifyconfig);
setState(() => _amplifyConfigured = true);
} catch (e) {
print(e);
setState(() => _amplifyConfigured = false);
}
}
@override
void initState() {
super.initState();
_configureAmplify();
}
...
First, we create an instance of the AmplifyAuthCognito
and then we add it amplify
instance. Remember the auto-generated file during the amplify init
? In the final step, we add our amplifyconfig
value to _amplifyInstance
. If this process is successfully completed we set _amplifyConfigured
to true. If some error occurs we will set to false.
Now let’s run the app to see everything is working fine.
⚠ Note: If you do a hot restart, you may get the following exception.
PlatformException(AmplifyException, User-Agent was already configured successfully., {cause: null, recoverySuggestion: User-Agent is configured internally during Amplify configuration. This method should not be called externally.}, null)
It’s because we have already configured Amplify and the library does not detect the hot restart. In the future releases, they will fix this bug.
It’s time to create the login and sign up screens. I am going to create the screens inside the screen folder as follows.
- The file
email_validator.dart
contains a function that validates the string is an email or not.
Let’s implement the signup_screen.dart
first as follows.
import 'package:todo_amplify/screens/email_confirmation_screen.dart';
import 'package:todo_amplify/util/email_validator.dart';
import 'package:flutter/material.dart';
class SignUpScreen extends StatelessWidget {
SignUpScreen({Key key}) : super(key: key);
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text("Sign up"),
),
body: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(labelText: "Email"),
controller: _emailController,
validator: (value) =>
!validateEmail(value) ? "Email is Invalid" : null,
),
TextFormField(
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(labelText: "Password"),
obscureText: true,
controller: _passwordController,
validator: (value) => value.isEmpty
? "Password is invalid"
: value.length < 9
? "Password must contain at least 8 characters"
: null,
),
SizedBox(
height: 20,
),
RaisedButton(
child: Text("CREATE ACCOUNT"),
onPressed: () => _createAccountOnPressed(context),
color: Theme.of(context).primaryColor,
colorBrightness: Theme.of(context).primaryColorBrightness),
],
),
),
),
);
}
Future<void> _createAccountOnPressed(BuildContext context) async {
if (_formKey.currentState.validate()) {
final email = _emailController.text;
final password = _passwordController.text;
// TODO: Implment sign-up process
}
}
void _gotToEmailConfirmationScreen(BuildContext context, String email) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EmailConfirmationScreen(email: email),
),
);
}
}
As you can see when the user press the sign up button _createAccountOnPressed
will be executed. Let’s implement the method as follows.
Future<void> _createAccountOnPressed(BuildContext context) async {
if (_formKey.currentState.validate()) {
final email = _emailController.text.trim();
final password = _passwordController.text;
/// In this user attribute, you can define the custom fields associated with the user.
/// For example birthday, telephone number, etc
Map<String, dynamic> userAttributes = {
"email": email,
};
try {
final result = await Amplify.Auth.signUp(
username: email,
password: password,
options: CognitoSignUpOptions(userAttributes: userAttributes),
);
if (result.isSignUpComplete) {
_gotToEmailConfirmationScreen(context, email);
}
} on AuthError catch (e) {
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text(e.cause),
),
);
}
}
}
- First we get the email and password from the controllers.
- Since we are using email and password-based authentication we have to create a map called
userAttributes
to store the email. This map can be used to store user-related data such as phone number, birthday, etc along with the user’s credentials. - Then we execute the method
Amplify.Auth.signUp
to sign up the user to the AWS Cognito user pool. We use email as the username. But don’t forget to adduserAttributes
to this method. - If some error occurred, for now, we show it using a SnackBar.
- If the user successfully signs up AWS will send an email confirmation code to the given email. So we should redirect the user to a new screen called
email_confimation_screen
.
Let’s implement email_confimation_screen.dart
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:todo_amplify/screens/main_screen.dart';
import 'package:flutter/material.dart';
class EmailConfirmationScreen extends StatelessWidget {
final String email;
EmailConfirmationScreen({
Key key,
@required this.email,
}) : super(key: key);
final _scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController _confirmationCodeController =
TextEditingController();
final _formKey = GlobalKey<FormFieldState>();
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text("Confirm your email"),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
child: Column(
children: [
Text(
"An email confirmation code is sent to $email. Please type the code to confirm your email.",
style: Theme.of(context).textTheme.headline6,
),
TextFormField(
keyboardType: TextInputType.number,
controller: _confirmationCodeController,
decoration: InputDecoration(labelText: "Confirmation Code"),
validator: (value) => value.length != 6
? "The confirmation code is invalid"
: null,
),
RaisedButton(
onPressed: () => _submitCode(context),
child: Text("CONFIRM"),
),
],
),
),
),
);
}
Future<void> _submitCode(BuildContext context) async {
if(_formKey.currentState.validate()){
final confirmationCode = _confirmationCodeController.text;
// TODO: Submit the code to Amplify
}
}
void _gotoMainScreen(BuildContext context) {
Navigator.push(context, MaterialPageRoute(builder: (_) => MainScreen()));
}
}
A confirmation code is 6 digit code. So after validating the code we can send the code to the Amplify as follows.
Future<void> _submitCode(BuildContext context) async {
if (_formKey.currentState.validate()) {
final confirmationCode = _confirmationCodeController.text;
try {
final SignUpResult response = await Amplify.Auth.confirmSignUp(
username: email,
confirmationCode: confirmationCode,
);
if (response.isSignUpComplete) {
_gotoMainScreen(context);
}
} on AuthError catch (e) {
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text(e.cause),
),
);
}
}
}
We can confirm the email using Amplify.Auth.confirmSignUp
. If the user email confirmation process is successfully complemented, we can redirect the user to the main screen.
Now let’s implement login_screen.dart
.
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:todo_amplify/screens/main_screen.dart';
import 'package:todo_amplify/screens/signup_screen.dart';
import 'package:todo_amplify/util/email_validator.dart';
import 'package:flutter/material.dart';
class LoginScreen extends StatelessWidget {
LoginScreen({Key key}) : super(key: key);
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(50),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(labelText: "Email"),
controller: _emailController,
validator: (value) =>
!validateEmail(value) ? "Email is Invalid" : null,
),
TextFormField(
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(labelText: "Password"),
obscureText: true,
controller: _passwordController,
validator: (value) =>
value.isEmpty ? "Password is invalid" : null,
),
SizedBox(
height: 20,
),
RaisedButton(
child: Text("LOG IN"),
onPressed: () => _loginButtonOnPressed(context),
color: Theme.of(context).primaryColor,
),
OutlineButton(
child: Text("Create New Account"),
onPressed: () => _gotoSignUpScreen(context),
color: Theme.of(context).primaryColor,
),
],
),
),
),
);
}
Future<void> _loginButtonOnPressed(BuildContext context) async {
if (_formKey.currentState.validate()) {
//TODO: Login code
}
}
void _gotoSignUpScreen(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SignUpScreen(),
),
);
}
}
When the user presses the login button _loginButtonOnPressed
will be executed.
Future<void> _loginButtonOnPressed(BuildContext context) async {
if (_formKey.currentState.validate()) {
/// Login code
try {
final email = _emailController.text.trim();
final password = _passwordController.text;
final response = await Amplify.Auth.signIn(
username: email,
password: password,
);
if (response.isSignedIn) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainScreen(),
),
);
}
} on AuthError catch (e) {
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text(e.cause),
),
);
}
}
}
We can perform a login request using Amplify.Auth.signIn
method.
Posted on December 31, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.