Offline Programmer
Posted on January 14, 2021
One of my goals this year is to publish my App KidzTokenz on Apple App Store. For that, I am planning to use Flutter and AWSAmplify.
In this post, we will integrate the Auth category into a Flutter App and run it on iOS.
Amplify Setup
I am going to use Amplify Admin UI to create the backend. Use the instruction I shared in the previous post (link below) to create a project of the name (AuthFlutter).
Hey @AWSAmplify, Is this food?
Offline Programmer ・ Dec 15 '20
Configure and deploy the Authentication. We are going to use (Email) for verification.
Project Setup
Using Terminal, create a new flutter project by running the command below
flutter create auth_for_flutter
Next, let's open the project folder using the VS Code. Make sure the integrated terminal is in the ios folder.
Run the following commands, and you will get a Podfile in the ios folder.
sudo gem install cocoapods
pod init
Update the Podile as below
platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
In XCode, select your project in the Project Navigator and select the “Info” tab. Set the Deployment Target to at least 13.0.
Open the App‘s pubspec.yaml and add the following dependencies below the line “sdk:flutter”.
dependencies:
flutter:
sdk: flutter
amplify_core: '<1.0.0'
amplify_auth_cognito: '<1.0.0'
Run the command below in the in ios folder
pod install
This is going to install the required Pods.
Pull down the Amplify backend we created above by running the pull command from the Admin UI in the root folder of the App
Answer the prompted questions, and once complete, you will get a confirmation as below.
Added backend environment config object to your project.
Run 'amplify pull' to sync upstream changes.
Update the app‘s pubspec.yaml to add the image file we are going to use
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/applogo.png
Run the App to make sure it builds successfully
The Implementation
We are going to use an enum for the authentication's status. Create the file below in the (lib) folder
account_screens_enum.dart
enum AccountStatus {
sign_in,
sign_up,
reset_password,
confirm_code,
main_screen
}
Next, let's create the widgets in (lib\widgets) folder.
sign_up.dart
To create an account.
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:auth_for_flutter/account_screens_enum.dart';
import 'package:auth_for_flutter/widgets/confirm_signup.dart';
import 'package:auth_for_flutter/widgets/error_view.dart';
import 'package:flutter/material.dart';
class SignUpView extends StatefulWidget {
final Function _displayAccountWidget;
const SignUpView(this._displayAccountWidget);
@override
_SignUpViewState createState() => _SignUpViewState();
}
class _SignUpViewState extends State<SignUpView> {
final emailController = TextEditingController();
final passwordController = TextEditingController();
bool _isSignedUp = false;
DateTime signupDate;
String _signUpError = "";
List<String> _signUpExceptions = [];
@override
void initState() {
super.initState();
}
void _setError(AuthError error) {
setState(() {
_signUpError = error.cause;
_signUpExceptions.clear();
error.exceptionList.forEach((el) {
_signUpExceptions.add(el.exception);
});
});
}
void _signUp(BuildContext context) async {
try {
Map<String, dynamic> userAttributes = {
"email": emailController.text.trim(),
"preferred_username": emailController.text.trim(),
// additional attributes as needed
};
SignUpResult res = await Amplify.Auth.signUp(
username: emailController.text.trim(),
password: passwordController.text.trim(),
options: CognitoSignUpOptions(userAttributes: userAttributes));
print(res.isSignUpComplete);
setState(() {
_isSignedUp = true;
});
} on AuthError catch (error) {
_setError(error);
}
}
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
children: [
Visibility(
visible: !_isSignedUp,
child: Column(children: [
TextFormField(
enableSuggestions: false,
decoration: const InputDecoration(
icon: Icon(Icons.email),
hintText: 'Email',
labelText: 'Email *',
),
controller: emailController,
keyboardType: TextInputType.emailAddress,
),
TextFormField(
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: const InputDecoration(
icon: Icon(Icons.lock),
hintText: 'Password',
labelText: 'Password *',
),
controller: passwordController,
),
const Padding(padding: EdgeInsets.all(10.0)),
FlatButton(
textColor:
Colors.black, // Theme.of(context).primaryColor,
color: Colors.amber,
onPressed: () => _signUp(context),
child: Text(
'Create Account',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
FlatButton(
height: 5,
onPressed: _displaySignIn,
child: Text(
'Already registered? Sign In',
style: Theme.of(context).textTheme.subtitle2,
),
),
]),
),
Visibility(
visible: _isSignedUp,
child: Column(children: [
ConfirmSignup(emailController.text.trim(), _setError),
])),
ErrorView(_signUpError, _signUpExceptions)
],
),
),
),
],
),
);
}
void _displaySignIn() {
widget._displayAccountWidget(AccountStatus.sign_in.index);
}
}
confirm_signup.dart
To submit the signup confirmation code.
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:auth_for_flutter/screens/next_screen.dart';
import 'package:flutter/material.dart';
class ConfirmSignup extends StatelessWidget {
final codeController = TextEditingController();
final String userName;
final Function setError;
ConfirmSignup(this.userName, this.setError);
void _skip_confirm_signup(BuildContext context) {
_go_to_NextScreen(context);
}
void _confirm_signup(BuildContext context) async {
try {
SignUpResult res = await Amplify.Auth.confirmSignUp(
username: this.userName,
confirmationCode: codeController.text.trim());
_go_to_NextScreen(context);
} on AuthError catch (e) {
setError(e);
}
}
void _go_to_NextScreen(BuildContext context) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) {
return NextScreen();
},
),
);
}
@override
Widget build(BuildContext context) {
return Container(
// decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
padding: EdgeInsets.all(5),
child: Column(
children: [
TextFormField(
controller: codeController,
decoration: const InputDecoration(
icon: Icon(Icons.confirmation_number),
hintText: 'The code we sent you',
labelText: 'Confirmation Code *',
)),
FlatButton(
textColor: Colors.black, // Theme.of(context).primaryColor,
color: Colors.amber,
onPressed: () => _confirm_signup(context),
child: Text(
'Confirm Sign Up',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
);
}
}
reset_password.dart
To request a password reset operation.
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:auth_for_flutter/account_screens_enum.dart';
import 'package:auth_for_flutter/widgets/confirm_reset_password.dart';
import 'package:auth_for_flutter/widgets/error_view.dart';
import 'package:flutter/material.dart';
class ResetPasswordView extends StatefulWidget {
final Function _displayAccountWidget;
const ResetPasswordView(this._displayAccountWidget);
@override
_ResetPasswordViewState createState() => _ResetPasswordViewState();
}
class _ResetPasswordViewState extends State<ResetPasswordView> {
final emailController = TextEditingController();
bool _isPasswordReset = false;
String _signUpError = "";
List<String> _signUpExceptions = [];
@override
void initState() {
super.initState();
}
void _setError(AuthError error) {
setState(() {
_signUpError = error.cause;
_signUpExceptions.clear();
error.exceptionList.forEach((el) {
_signUpExceptions.add(el.exception);
});
});
}
void _resetPassword(BuildContext context) async {
try {
ResetPasswordResult res = await Amplify.Auth.resetPassword(
username: emailController.text.trim(),
);
setState(() {
_isPasswordReset = true;
});
} on AuthError catch (e) {
setState(() {
_signUpError = e.cause;
_signUpExceptions.clear();
e.exceptionList.forEach((el) {
_signUpExceptions.add(el.exception);
});
});
}
}
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
children: [
Visibility(
visible: !_isPasswordReset,
child: Column(children: [
TextFormField(
enableSuggestions: false,
decoration: const InputDecoration(
icon: Icon(Icons.email),
hintText: 'Email',
labelText: 'Email *',
),
controller: emailController,
keyboardType: TextInputType.emailAddress,
),
const Padding(padding: EdgeInsets.all(10.0)),
FlatButton(
textColor:
Colors.black, // Theme.of(context).primaryColor,
color: Colors.amber,
onPressed: () => _resetPassword(context),
child: Text(
'Reset Password',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
FlatButton(
height: 5,
onPressed: _displayCreateAccount,
child: Text(
'Create Account',
style: Theme.of(context).textTheme.subtitle2,
),
),
]),
),
Visibility(
visible: _isPasswordReset,
child: Column(children: [
ConfirmResetPassword(
emailController.text.trim(), _setError),
])),
ErrorView(_signUpError, _signUpExceptions)
],
),
),
),
],
),
);
}
void _displayCreateAccount() {
widget._displayAccountWidget(AccountStatus.sign_up.index);
}
}
confirm_reset_password.dart
To submit the confirmation code and the new password.
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:auth_for_flutter/screens/next_screen.dart';
import 'package:flutter/material.dart';
class ConfirmResetPassword extends StatelessWidget {
final codeController = TextEditingController();
final emailController = TextEditingController();
final passwordController = TextEditingController();
final String userName;
final Function setError;
ConfirmResetPassword(this.userName, this.setError);
void _confirm_password_reset(BuildContext context) async {
try {
await Amplify.Auth.confirmPassword(
username: this.userName,
newPassword: passwordController.text.trim(),
confirmationCode: codeController.text.trim());
_go_to_NextScreen(context);
} on AuthError catch (e) {
setError(e);
}
}
void _go_to_NextScreen(BuildContext context) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) {
return NextScreen();
},
),
);
}
@override
Widget build(BuildContext context) {
return Container(
// decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
padding: EdgeInsets.all(5),
child: Column(
children: [
TextFormField(
enableSuggestions: false,
decoration: const InputDecoration(
icon: Icon(Icons.email),
hintText: 'Email',
labelText: 'Email *',
),
controller: emailController,
keyboardType: TextInputType.emailAddress,
),
TextFormField(
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: const InputDecoration(
icon: Icon(Icons.lock),
hintText: 'New Password',
labelText: 'New Password *',
),
controller: passwordController,
),
TextFormField(
controller: codeController,
decoration: const InputDecoration(
icon: Icon(Icons.confirmation_number),
hintText: 'The code we sent you',
labelText: 'Confirmation Code *',
)),
FlatButton(
textColor: Colors.black, // Theme.of(context).primaryColor,
color: Colors.amber,
onPressed: () => _confirm_password_reset(context),
child: Text(
'Reset Password & Sign In',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
);
}
}
sign_in.dart
The sign-in operation
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:auth_for_flutter/account_screens_enum.dart';
import 'package:auth_for_flutter/screens/next_screen.dart';
import 'package:auth_for_flutter/widgets/error_view.dart';
import 'package:flutter/material.dart';
class SignInView extends StatefulWidget {
final Function _displayAccountWidget;
const SignInView(this._displayAccountWidget);
@override
_SignInViewState createState() => _SignInViewState();
}
class _SignInViewState extends State<SignInView> {
final emailController = TextEditingController();
final passwordController = TextEditingController();
String _signUpError = "";
List<String> _signUpExceptions = [];
@override
void initState() {
super.initState();
}
void _setError(AuthError error) {
setState(() {
_signUpError = error.cause;
_signUpExceptions.clear();
error.exceptionList.forEach((el) {
_signUpExceptions.add(el.exception);
});
});
}
void _signIn() async {
// Sign out before in case a user is already signed in
// If a user is already signed in - Amplify.Auth.signIn will throw an exception
try {
await Amplify.Auth.signOut();
} on AuthError catch (e) {
print(e);
}
try {
SignInResult res = await Amplify.Auth.signIn(
username: emailController.text.trim(),
password: passwordController.text.trim());
_go_to_NextScreen(context);
} on AuthError catch (e) {
setState(() {
_signUpError = e.cause;
_signUpExceptions.clear();
e.exceptionList.forEach((el) {
_signUpExceptions.add(el.exception);
});
});
}
}
void _go_to_NextScreen(BuildContext context) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) {
return NextScreen();
},
),
);
}
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
// wrap your Column in Expanded
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
children: [
TextFormField(
controller: emailController,
decoration: const InputDecoration(
icon: Icon(Icons.email),
hintText: 'Enter your email',
labelText: 'Email *',
),
),
TextFormField(
obscureText: true,
controller: passwordController,
decoration: const InputDecoration(
icon: Icon(Icons.lock),
hintText: 'Enter your password',
labelText: 'Password *',
),
),
const Padding(padding: EdgeInsets.all(10.0)),
FlatButton(
textColor: Colors.black, // Theme.of(context).primaryColor,
color: Colors.amber,
onPressed: _signIn,
child: const Text(
'Sign In',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
height: 5,
onPressed: _displayCreateAccount,
child: Text(
'Create Account',
style: Theme.of(context).textTheme.subtitle2,
),
),
FlatButton(
height: 5,
onPressed: _displayResetPassword,
child: Text(
'Reset Password',
style: Theme.of(context).textTheme.subtitle2,
),
),
],
),
ErrorView(_signUpError, _signUpExceptions)
],
),
),
),
],
),
);
}
void _displayCreateAccount() {
widget._displayAccountWidget(AccountStatus.sign_up.index);
}
void _displayResetPassword() {
widget._displayAccountWidget(AccountStatus.reset_password.index);
}
}
error_view.dart
Display the error messages.
import 'package:flutter/material.dart';
class ErrorView extends StatelessWidget {
final String error;
final List<String> exceptions;
ErrorView(this.error, this.exceptions);
@override
Widget build(BuildContext context) {
// We do not recognize your username and/or password. Please try again.
if (error.isNotEmpty || exceptions.length > 0) {
return Column(children: <Widget>[
Text('Error: $error',
textAlign: TextAlign.center,
overflow: TextOverflow.visible,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).errorColor,
)),
if (exceptions.length > 0) ...[_showExceptions(context)]
]);
} else {
return Container();
}
}
_showExceptions(context) {
return Column(
children: exceptions
.map((item) => new Text(item + " ",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).errorColor,
)))
.toList());
}
}
We will have three screens in the App. Create the screens below in the (lib\screens) folder
loading_screen.dart
import 'package:flutter/material.dart';
class LoadingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BatMan App')),
body: Container(
color: Color(0xff90CCE6),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
color: Color(0xffE1E5E4),
height: 200,
child: Image.asset(
'assets/images/applogo.png',
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: CircularProgressIndicator(),
),
),
],
),
),
),
);
}
}
main_screen.dart
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:auth_for_flutter/account_screens_enum.dart';
import 'package:auth_for_flutter/screens/next_screen.dart';
import 'package:auth_for_flutter/widgets/reset_password.dart';
import 'package:auth_for_flutter/widgets/sign_in.dart';
import 'package:auth_for_flutter/widgets/sign_up.dart';
import 'package:flutter/material.dart';
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
var _accountWidget;
@override
initState() {
super.initState();
_fetchSession();
}
void _fetchSession() async {
// Sign out before in case a user is already signed in
try {
await Amplify.Auth.signOut();
} on AuthError catch (e) {
print(e);
}
_accountWidget = AccountStatus.sign_in.index;
_displayAccountWidget(_accountWidget);
}
void _go_to_NextScreen(BuildContext context) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) {
return NextScreen();
},
),
);
}
void _displayAccountWidget(int accountStatus) {
setState(() {
_accountWidget = AccountStatus.values[accountStatus];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('BatMan App'),
),
body: Container(
color: Color(0xffE1E5E4),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
color: Color(0xffE1E5E4),
height: 200,
child: Image.asset(
'assets/images/applogo.png',
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(children: [
Visibility(
visible: _accountWidget == AccountStatus.sign_in,
child: SignInView(_displayAccountWidget),
),
Visibility(
visible: _accountWidget == AccountStatus.sign_up,
child: SignUpView(_displayAccountWidget),
),
Visibility(
visible: _accountWidget == AccountStatus.reset_password,
child: ResetPasswordView(_displayAccountWidget),
),
Visibility(
visible: _accountWidget == AccountStatus.main_screen,
child: NextScreen(),
),
]),
),
],
),
),
),
);
}
}
next_screen.dart
import 'package:auth_for_flutter/screens/main_screen.dart';
import 'package:flutter/material.dart';
class NextScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BatMan App')),
body: Container(
color: Color(0xffE1E5E4),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
color: Color(0xffE1E5E4),
height: 200,
child: Image.asset(
'assets/images/applogo.png',
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: Text(
'I\'m BATMAN',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
),
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: RaisedButton(
color: Colors.lightBlue,
onPressed: () => _signOut(context),
child: Text(
'Sign Out',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
),
),
),
],
),
),
),
);
}
void _signOut(BuildContext context) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) {
return MainScreen();
},
),
);
}
}
Run the App
Check the code here
Follow me on Twitter for more tips about #coding, #learning, #technology...etc.
Check my Apps on Google Play
Cover image Obi Onyeador on Unsplash
Posted on January 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.