flutter_registration/lib/src/auth_screen.dart

422 lines
14 KiB
Dart
Raw Normal View History

2022-11-01 09:19:20 +01:00
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
2024-08-08 10:57:20 +02:00
import "dart:async";
import "dart:collection";
import "package:flutter/material.dart";
import "package:flutter_registration/flutter_registration.dart";
2022-09-20 15:51:22 +02:00
2024-02-19 13:31:12 +01:00
/// A widget for handling multi-step authentication processes.
2022-09-20 15:51:22 +02:00
class AuthScreen extends StatefulWidget {
2024-02-19 13:31:12 +01:00
/// Constructs an [AuthScreen] object.
///
/// [appBarTitle] specifies the title of the app bar.
///
2024-08-08 10:57:20 +02:00
/// [onFinish] is a function called upon
/// completion of the authentication process.
2024-02-19 13:31:12 +01:00
///
/// [steps] is a list of authentication steps to be completed.
///
/// [submitBtnTitle] specifies the title of the submit button.
///
/// [nextBtnTitle] specifies the title of the next button.
///
/// [previousBtnTitle] specifies the title of the previous button.
///
/// [customAppBar] allows customization of the app bar.
///
/// [buttonMainAxisAlignment] specifies the alignment of the buttons.
///
/// [customBackgroundColor] allows customization of the background color.
///
/// [nextButtonBuilder] allows customization of the next button.
///
/// [previousButtonBuilder] allows customization of the previous button.
///
2024-08-08 10:57:20 +02:00
/// [titleWidget] specifies a custom widget
/// to be displayed at the top of the screen.
2024-02-19 13:31:12 +01:00
///
/// [loginButton] specifies a custom login button widget.
///
/// [titleFlex] specifies the flex value for the title widget.
///
/// [formFlex] specifies the flex value for the form widget.
///
/// [beforeTitleFlex] specifies the flex value before the title widget.
///
/// [afterTitleFlex] specifies the flex value after the title widget.
2024-08-08 10:57:20 +02:00
2022-09-20 15:51:22 +02:00
const AuthScreen({
required this.appBarTitle,
2022-09-20 15:51:22 +02:00
required this.steps,
required this.submitBtnTitle,
required this.nextBtnTitle,
required this.previousBtnTitle,
2022-09-20 15:51:22 +02:00
required this.onFinish,
2022-09-28 09:23:41 +02:00
this.customAppBar,
this.buttonMainAxisAlignment,
this.customBackgroundColor,
this.nextButtonBuilder,
this.previousButtonBuilder,
this.titleWidget,
this.loginButton,
this.titleFlex,
this.formFlex,
this.beforeTitleFlex,
this.afterTitleFlex,
2024-04-22 13:01:35 +02:00
this.maxFormWidth,
2024-08-08 10:57:20 +02:00
super.key,
}) : assert(steps.length > 0, "At least one step is required");
2022-09-20 15:51:22 +02:00
2024-08-08 10:57:20 +02:00
/// The title of the app bar.
final String appBarTitle;
2024-08-08 10:57:20 +02:00
/// A function called upon completion of the authentication process.
final Future<void> Function({
required HashMap<String, dynamic> values,
2023-10-03 14:38:52 +02:00
required void Function(int? pageToReturn) onError,
}) onFinish;
2024-08-08 10:57:20 +02:00
/// The authentication steps to be completed.
2022-09-20 15:51:22 +02:00
final List<AuthStep> steps;
2024-08-08 10:57:20 +02:00
/// The title of the submit button.
2022-09-20 15:51:22 +02:00
final String submitBtnTitle;
2024-08-08 10:57:20 +02:00
/// The title of the next button.
final String nextBtnTitle;
2024-08-08 10:57:20 +02:00
/// The title of the previous button.
final String previousBtnTitle;
2024-08-08 10:57:20 +02:00
/// A custom app bar widget.
2022-09-28 09:23:41 +02:00
final AppBar? customAppBar;
2024-08-08 10:57:20 +02:00
/// The alignment of the buttons.
final MainAxisAlignment? buttonMainAxisAlignment;
2024-08-08 10:57:20 +02:00
/// The background color of the screen.
final Color? customBackgroundColor;
2024-08-08 10:57:20 +02:00
/// A custom widget for the button.
final Widget Function(
Future<void> Function()? onPressed,
String label,
int step,
// ignore: avoid_positional_boolean_parameters
bool enabled,
)? nextButtonBuilder;
/// A custom widget for the button.
final Widget? Function(VoidCallback onPressed, String label, int step)?
previousButtonBuilder;
2024-08-08 10:57:20 +02:00
/// A custom widget for the title.
final Widget? titleWidget;
2024-08-08 10:57:20 +02:00
/// A custom widget for the login button.
final Widget? loginButton;
2024-08-08 10:57:20 +02:00
/// The flex value for the title widget.
final int? titleFlex;
2024-08-08 10:57:20 +02:00
/// The flex value for the form widget.
final int? formFlex;
2024-08-08 10:57:20 +02:00
/// The flex value before the title widget.
final int? beforeTitleFlex;
2024-08-08 10:57:20 +02:00
/// The flex value after the title widget.
final int? afterTitleFlex;
2024-08-08 10:57:20 +02:00
/// The maximum width of the form.
2024-04-22 13:01:35 +02:00
final double? maxFormWidth;
2022-09-20 15:51:22 +02:00
@override
State<AuthScreen> createState() => _AuthScreenState();
}
2024-02-19 13:31:12 +01:00
/// The state for [AuthScreen].
2022-09-20 15:51:22 +02:00
class _AuthScreenState extends State<AuthScreen> {
final _formKey = GlobalKey<FormState>();
final _pageController = PageController();
final _animationDuration = const Duration(milliseconds: 300);
final _animationCurve = Curves.ease;
bool _formValid = false;
2022-09-20 15:51:22 +02:00
2024-02-19 13:31:12 +01:00
/// Gets the app bar.
2022-09-28 09:23:41 +02:00
AppBar get _appBar =>
widget.customAppBar ??
AppBar(
2024-04-19 10:14:08 +02:00
backgroundColor: const Color(0xffFAF9F6),
title: Text(widget.appBarTitle),
2022-09-28 09:23:41 +02:00
);
2024-02-19 13:31:12 +01:00
/// Handles previous button press.
void onPrevious() {
FocusScope.of(context).unfocus();
_validate(_pageController.page!.toInt() - 1);
2024-08-08 10:57:20 +02:00
unawaited(
_pageController.previousPage(
duration: _animationDuration,
curve: _animationCurve,
),
);
}
2024-02-19 13:31:12 +01:00
/// Handles next button press.
Future<void> onNext(AuthStep step) async {
if (!_formKey.currentState!.validate()) {
return;
}
_formKey.currentState!.save();
FocusScope.of(context).unfocus();
if (widget.steps.last == step) {
var values = HashMap<String, dynamic>();
for (var step in widget.steps) {
for (var field in step.fields) {
values[field.name] = field.value;
}
}
await widget.onFinish(
values: values,
2024-08-08 10:57:20 +02:00
onError: (int? pageToReturn) {
if (pageToReturn == null) {
return;
}
_pageController.animateToPage(
pageToReturn,
duration: _animationDuration,
curve: _animationCurve,
);
},
);
return;
} else {
_validate(_pageController.page!.toInt() + 1);
2024-08-08 10:57:20 +02:00
unawaited(
_pageController.nextPage(
duration: _animationDuration,
curve: _animationCurve,
),
);
}
}
2024-02-19 13:31:12 +01:00
/// Validates the current step.
void _validate(int currentPage) {
2024-08-08 10:57:20 +02:00
var isStepValid = true;
// Loop through each field in the current step
for (var field in widget.steps[currentPage].fields) {
for (var validator in field.validators) {
2024-08-08 10:57:20 +02:00
var validationResult = validator(field.value);
if (validationResult != null) {
// If any validator returns an error, mark step as invalid and break
isStepValid = false;
break;
}
}
if (!isStepValid) {
break; // No need to check further fields if one is already invalid
}
}
setState(() {
_formValid = isStepValid;
});
}
2022-09-20 15:51:22 +02:00
@override
Widget build(BuildContext context) {
2024-08-08 10:57:20 +02:00
var theme = Theme.of(context);
return Scaffold(
backgroundColor: widget.customBackgroundColor ?? const Color(0xffFAF9F6),
appBar: _appBar,
body: SafeArea(
child: Form(
key: _formKey,
child: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
children: <Widget>[
for (var currentStep = 0;
currentStep < widget.steps.length;
currentStep++)
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.titleWidget != null) ...[
Expanded(
flex: widget.titleFlex ?? 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
2024-04-19 10:14:08 +02:00
Expanded(
2024-08-08 10:57:20 +02:00
flex: widget.beforeTitleFlex ?? 2,
child: Container(),
2024-02-19 13:31:12 +01:00
),
2024-08-08 10:57:20 +02:00
widget.titleWidget!,
Expanded(
flex: widget.afterTitleFlex ?? 2,
child: Container(),
),
2024-08-08 10:57:20 +02:00
],
),
),
],
Expanded(
flex: widget.formFlex ?? 3,
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.maxFormWidth ?? 300,
),
2024-08-08 10:57:20 +02:00
child: Column(
2024-04-19 10:14:08 +02:00
children: [
2024-08-08 10:57:20 +02:00
for (AuthField field
in widget.steps[currentStep].fields) ...[
if (field.title != null) ...[
wrapWithDefaultStyle(
style: theme.textTheme.headlineLarge!,
widget: field.title!,
),
2024-04-19 10:14:08 +02:00
],
2024-08-08 10:57:20 +02:00
field.build(context, () {
_validate(currentStep);
}),
],
2024-04-19 10:14:08 +02:00
],
),
2024-08-08 10:57:20 +02:00
),
2024-04-19 10:14:08 +02:00
),
2024-08-08 10:57:20 +02:00
),
Column(
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
child: Row(
mainAxisAlignment: widget.steps.first !=
widget.steps[currentStep]
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.end,
children: [
if (widget.steps.first !=
widget.steps[currentStep]) ...[
widget.previousButtonBuilder?.call(
onPrevious,
widget.previousBtnTitle,
currentStep,
) ??
_stepButton(
buttonText: widget.previousBtnTitle,
onTap: () async => onPrevious(),
),
const SizedBox(
width: 8,
),
],
widget.nextButtonBuilder?.call(
!_formValid
? null
: () async {
await onNext(
widget.steps[currentStep],
);
},
widget.steps.last ==
widget.steps[currentStep]
? widget.submitBtnTitle
: widget.nextBtnTitle,
currentStep,
_formValid,
) ??
_stepButton(
buttonText: widget.steps.last ==
widget.steps[currentStep]
? widget.submitBtnTitle
: widget.nextBtnTitle,
onTap: () async {
await onNext(
widget.steps[currentStep],
);
},
),
],
),
),
),
const SizedBox(
height: 8,
),
if (widget.loginButton != null)
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: widget.loginButton,
),
],
),
2024-04-19 10:14:08 +02:00
],
),
2024-08-08 10:57:20 +02:00
],
),
),
),
);
}
Widget _stepButton({
required String buttonText,
required Future Function()? onTap,
}) {
var theme = Theme.of(context);
return Flexible(
child: InkWell(
onTap: onTap,
child: Container(
width: double.infinity,
constraints: const BoxConstraints(
maxWidth: 180,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(
0xff979797,
2024-02-19 13:31:12 +01:00
),
),
2024-08-08 10:57:20 +02:00
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(
buttonText,
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
),
),
);
}
2024-08-08 10:57:20 +02:00
Widget wrapWithDefaultStyle({
required Widget widget,
required TextStyle style,
}) =>
DefaultTextStyle(style: style, child: widget);
2022-09-20 15:51:22 +02:00
}