Unauthenticated Routes
Providing a builder value of Authenticator.builder()
to your MaterialApp will make all routes in your application require authentication. The Authenticator can also be used to protect a subset of routes, and leave other routes unauthenticated.
In the example below, the default route (/
) will not require authentication. When the app launches, the HomeScreen widget will show to authenticated and unauthenticated users. /profile
returns an AuthenticatedView widget, with a child of ProfileScreen()
. This route will require authentication. If an unauthenticated user navigates to this route, they will see the authenticator. As soon as they authenticate, they will see the ProfileScreen widget.
import 'package:amplify_authenticator/amplify_authenticator.dart';
import 'package:flutter/material.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
void initState() {
super.initState();
// configure amplify
}
Widget build(BuildContext context) {
return Authenticator(
child: MaterialApp(
initialRoute: '/',
routes: {
'/': (BuildContext context) {
return const Center(child: Text('Home'));
},
'/profile': (BuildContext context) {
return const AuthenticatedView(
child: Center(child: Text('Profile')),
);
},
},
),
);
}
}
AuthenticatedView
can be used along side the routing package of your choice. The example shows the same example using go_router.
import 'package:amplify_authenticator/amplify_authenticator.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
void initState() {
super.initState();
// configure amplify
}
static final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (BuildContext _, GoRouterState __) {
return const Center(child: Text('Home'));
},
),
GoRoute(
path: '/profile',
builder: (BuildContext _, GoRouterState __) {
return const AuthenticatedView(child: Center(child: Text('Profile')));
},
),
],
);
Widget build(BuildContext context) {
return Authenticator(
child: MaterialApp.router(
routerConfig: _router,
),
);
}
}
Internationalization (I18n)
To use localization, start by following the steps in the Flutter Localization guide. When the guide says to create an .arb file, enter the following into lib/l10n/amplify_en.arb
:
{
"signIn": "Sign In - Localized!",
"@signIn": {
"description": "Label of the button to sign in the user."
}
}
Then, to add support for another language, create a another file for that language. For example, to add Spanish support, create a file at lib/l10n/amplify_es.arb
, and add the following:
{
"signIn": "Iniciar SesiĆ³n"
}
This example only updates the 'Sign In' button. To find the full schema for Authenticator-compatible .arb
file(s), you can check the default .arb
files that come packaged with the Amplify Authenticator in the /src/l10n/src/
directory.
Next, import the following in the ./lib/main.dart
file:
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
Then create a ButtonResolver
and pass it to an AuthStringResolver
, and in turn pass this to the Authenticator API:
class LocalizedButtonResolver extends ButtonResolver {
const LocalizedButtonResolver();
/// Override the signIn function with a localized value
String signIn(BuildContext context) {
return AppLocalizations.of(context).signIn;
}
}
const stringResolver = AuthStringResolver(
buttons: LocalizedButtonResolver(),
);
return Authenticator(
stringResolver: stringResolver,
child: // ...
);
Finally, update your MaterialApp:
return MaterialApp(
title: 'Authenticator Demo',
theme: ThemeData.light(useMaterial3: true),
darkTheme: ThemeData.dark(useMaterial3: true),
localizationsDelegates: const [
AppLocalizations.delegate,
],
supportedLocales: const [
Locale('en'), // English
Locale('es'), // Spanish
],
builder: Authenticator.builder(),
home: const Scaffold(
body: Center(
child: Text('You are logged in!'),
),
),
);
There may be some analysis exceptions in your IDE because, as the Flutter instructions note, Flutter will automatically generate the files at build time. You may need to enable automatic code generation by setting generate: true
in your app's pubspec.yaml
file.
Labels & Text
Using a similar technique as Internationalization (I18n), you can customize the labels and text of the components.
Instead of implementing full fledged localization through the use of arb file, the example below shows how customize only the text within the "Sign In" button and the titles of the username, email and password fields:
/// Instantiate a ButtonResolver
class CustomButtonResolver extends ButtonResolver {
const CustomButtonResolver();
/// Override the signin function
String signin(BuildContext context) {
//the text of the signin button can be customized
return 'Login to Amplify';
}
}
/// Instantiate an InputResolver
class CustomInputResolver extends InputResolver {
const CustomInputResolver();
/// Override the title function
/// Since this function handles the title for numerous fields,
/// we recommend using a switch statement so that `super` can be called
/// in the default case.
String title(BuildContext context, InputField field) {
switch (field) {
case InputField.username:
return 'Custom Username Title';
case InputField.email:
return 'Custom Email Title';
case InputField.password:
return 'Custom Password Title';
case InputField.passwordConfirmation:
return 'Custom Password Confirmation Title';
default:
return super.title(context, field);
}
}
}
/// Instantiate an AuthStringResolver with your two custom resolvers
const stringResolver = AuthStringResolver(
buttons: CustomButtonResolver(),
inputs: CustomInputResolver(),
);
/// Pass the string resolver to the Authenticator API
final authenticator = Authenticator(
stringResolver: stringResolver,
child: MaterialApp(
title: 'Authenticator Demo',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(useMaterial3: true),
builder: Authenticator.builder(),
home: const Scaffold(
body: Center(
child: Text('You are logged in!'),
),
),
),
);
Sign Up Fields
The following example customizes the Sign Up screen by:
- Re-using the default Sign Up form fields
- Adding a "Website" field with custom validation
- Add a custom "Bio" field
build(BuildContext context) {
return Authenticator(
signUpForm: SignUpForm.custom(
fields: [
SignUpFormField.username(),
SignUpFormField.email(required: true),
SignUpFormField.custom(
required: true,
validator: ((value) {
if (value == null || value.isEmpty) {
return 'You must provide a website';
}
if (!value.contains('example.com')) {
return 'Your website must have a domain of example.com';
}
return null;
}),
title: 'Website',
attributeKey: CognitoUserAttributeKey.website,
),
SignUpFormField.custom(
title: 'Bio',
attributeKey: const CognitoUserAttributeKey.custom('bio'),
),
SignUpFormField.password(),
SignUpFormField.passwordConfirmation(),
],
),
child: MaterialApp(
builder: Authenticator.builder(),
home: const Scaffold(
body: Center(
child: Text('You are logged in!'),
),
),
),
);
}
Widget
Force New Password Fields
The following example customizes the Force New Password screen by:
- Re-using the default Force New Password form fields
- Appending a custom "Zone Info" text field
build(BuildContext context) {
return Authenticator(
confirmSignInNewPasswordForm: ConfirmSignInNewPasswordForm.custom(
fields: [
ConfirmSignInFormField.newPassword(),
ConfirmSignInFormField.confirmNewPassword(),
ConfirmSignInFormField.custom(
title: 'Zone Info',
attributeKey: CognitoUserAttributeKey.zoneinfo,
),
],
),
child: MaterialApp(
builder: Authenticator.builder(),
home: const Scaffold(
body: Center(
child: Text('You are logged in!'),
),
),
),
);
}
Widget
Form Field Customization
The Authenticator allows for customization of multiple aspects of the form fields. The sections below will describe several use cases, on how to modify these fields.
Default international dial code
Default Dial code customization is available via the defaultDialCode
prop in the dialCodeOptions
parameter. Changing the defaultDialCode
will change the default dial code in the phone number field for all forms, including the sign up, sign in, and forgot password forms.
The dial codes are a list of the ISO 3166-1 alpha-2 codes. For example, the United States is us
, and Canada is ca
.
A list of the codes can also be found in the DialCode
class.
build(BuildContext context) {
return Authenticator(
dialCodeOptions: const DialCodeOptions(defaultDialCode: DialCode.jp),
child: MaterialApp(
builder: Authenticator.builder(),
home: Scaffold(
body: Center(
child: Text('You are logged in!'),
),
),
),
),
}
Widget
TOTP Issuer
The TOTP issuer is the name that will be shown in TOTP applications preceding the account name. In most cases, this should be the name of your app. For example, if your app is called "My App", your user will see "My App" - "username" in their TOTP app. This can be customized by adding the TotpOptions
argument to the Authenticator component with a value for issuer
.
Note: Unless changed, the default issuer is the application name retrieved from the project configuration. The key for this value is CFBundleDisplayName
on iOS found in info.plist
, application:label
on Android found in AndroidManifest.xml
.
build(BuildContext context) {
return Authenticator(
totpOptions: const TotpOptions(issuer: 'Authenticator Demo'),
child: MaterialApp(
builder: Authenticator.builder(),
home: Scaffold(
body: Center(
child: Text('You are logged in!'),
),
),
),
),
}
Widget
Full UI Customization
In addition to customizing form fields and theming, you can also build a custom UI for one or more of the authenticator steps using a combination of prebuilt widgets from the amplify_authenticator package, and widgets that you build yourself. This can be used for simple use cases, such as adding a logo to the sign in screen, as well as more complex use cases.
This is done by providing an authenticatorBuilder
argument to the Authenticator. If you are familiar with widgets like StreamBuilder, FutureBuilder, or LayoutBuilder, this may feel familiar. authenticatorBuilder
is a builder method that takes arguments for the current BuildContext
and AuthenticatorState. AuthenticatorState is a representation of the state being managed by the authenticator. It contains all the form field data (username, email, etc.), methods to perform authentication actions (sign in, sign up, etc.), and methods to transition between steps.
Below is an example of how you can use authenticatorBuilder
to add a logo to both the sign up and sign in screen, as well as change the layout to replace the tab bar with a footer button.
This example uses several prebuilt widgets from the amplify_authenticator package, such as SignInForm()
, SignUpForm()
, SignInFormField.username()
, and SignInButton()
. All of the prebuilt widgets are integrated into the authenticator's state, meaning that you do not have to add an onChanged
callback for the Form Fields, or an onTap
callback for the Buttons. You can find a full list of the prebuilt forms, form fields, and buttons in the API docs.
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_authenticator/amplify_authenticator.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import 'amplifyconfiguration.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
void initState() {
super.initState();
_configureAmplify();
}
void _configureAmplify() async {
try {
await Amplify.addPlugin(AmplifyAuthCognito());
await Amplify.configure(amplifyconfig);
safePrint('Successfully configured');
} on Exception catch (e) {
safePrint('Error configuring Amplify: $e');
}
}
Widget build(BuildContext context) {
return Authenticator(
// `authenticatorBuilder` is used to customize the UI for one or more steps
authenticatorBuilder: (BuildContext context, AuthenticatorState state) {
switch (state.currentStep) {
case AuthenticatorStep.signIn:
return CustomScaffold(
state: state,
// A prebuilt Sign In form from amplify_authenticator
body: SignInForm(),
// A custom footer with a button to take the user to sign up
footer: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Don\'t have an account?'),
TextButton(
onPressed: () => state.changeStep(
AuthenticatorStep.signUp,
),
child: const Text('Sign Up'),
),
],
),
);
case AuthenticatorStep.signUp:
return CustomScaffold(
state: state,
// A prebuilt Sign Up form from amplify_authenticator
body: SignUpForm(),
// A custom footer with a button to take the user to sign in
footer: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Already have an account?'),
TextButton(
onPressed: () => state.changeStep(
AuthenticatorStep.signIn,
),
child: const Text('Sign In'),
),
],
),
);
case AuthenticatorStep.confirmSignUp:
return CustomScaffold(
state: state,
// A prebuilt Confirm Sign Up form from amplify_authenticator
body: ConfirmSignUpForm(),
);
case AuthenticatorStep.resetPassword:
return CustomScaffold(
state: state,
// A prebuilt Reset Password form from amplify_authenticator
body: ResetPasswordForm(),
);
case AuthenticatorStep.confirmResetPassword:
return CustomScaffold(
state: state,
// A prebuilt Confirm Reset Password form from amplify_authenticator
body: const ConfirmResetPasswordForm(),
);
default:
// Returning null defaults to the prebuilt authenticator for all other steps
return null;
}
},
child: MaterialApp(
builder: Authenticator.builder(),
home: const Scaffold(
body: Center(
child: Text('You are logged in!'),
),
),
),
);
}
}
/// A widget that displays a logo, a body, and an optional footer.
class CustomScaffold extends StatelessWidget {
const CustomScaffold({
super.key,
required this.state,
required this.body,
this.footer,
});
final AuthenticatorState state;
final Widget body;
final Widget? footer;
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Column(
children: [
// App logo
const Padding(
padding: EdgeInsets.only(top: 32),
child: Center(child: FlutterLogo(size: 100)),
),
Container(
constraints: const BoxConstraints(maxWidth: 600),
child: body,
),
],
),
),
),
persistentFooterButtons: footer != null ? [footer!] : null,
);
}
}
TOTP Setup
By utilizing the authenticatorBuilder
argument, you can also customize the TOTP setup experience. We make available two particularly helpful widgets: TotpSetupFormField.totpQrCode()
and TotpSetupFormField.totpCopyKey()
. In the example below, examine how you could customize the setup screen by conditionally hiding the QR code on mobile platforms.
build(BuildContext context) {
// Check if the current platform is mobile. If it is, we will hide the QR code
final isMobile = Theme.of(context).platform == TargetPlatform.iOS ||
Theme.of(context).platform == TargetPlatform.android;
return Authenticator(
// `authenticatorBuilder` is used to customize the UI for one or more steps
authenticatorBuilder: (BuildContext context, AuthenticatorState state) {
switch (state.currentStep) {
case AuthenticatorStep.continueSignInWithTotpSetup:
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Hide on mobile
if (!isMobile) ...[
const Text(
'If you are on a mobile device, you can scan the QR code '
'using the Authenticator app.',
),
// TOTP QR Code widget
TotpSetupFormField.totpQrCode(),
const SizedBox(height: 20),
],
const Text('Copy the key below into your Authenticator app'),
// TOTP copy key widget
TotpSetupFormField.totpCopyKey(),
const SizedBox(height: 20),
FilledButton(
onPressed: () => state.changeStep(
AuthenticatorStep.confirmSignInWithTotpMfaCode,
),
child: const Text('Continue'),
),
],
),
);
default:
// Returning null defaults to the prebuilt authenticator for all other steps
return null;
}
},
child: MaterialApp(
builder: Authenticator.builder(),
home: const Scaffold(
body: Center(
child: Text('You are logged in!'),
),
),
),
);
}
Widget