Compare commits

..

No commits in common. "master" and "1.2.0" have entirely different histories.

25 changed files with 485 additions and 1289 deletions

View file

@ -1,14 +0,0 @@
name: Iconica Standard Component Documentation Workflow
# Workflow Caller version: 1.0.0
on:
release:
types: [published]
workflow_dispatch:
jobs:
call-iconica-component-documentation-workflow:
uses: Iconica-Development/.github/.github/workflows/component-documentation.yml@master
secrets: inherit
permissions: write-all

5
.gitignore vendored
View file

@ -34,8 +34,3 @@ migrate_working_dir/
build/
.flutter-plugins
.flutter-plugins-dependencies
.metadata
# FVM Version Cache
.fvm/
.fvmrc

View file

@ -3,39 +3,6 @@ SPDX-FileCopyrightText: 2022 Iconica
SPDX-License-Identifier: GPL-3.0-or-later
-->
# 3.0.0
- fix: fixed the issue with the scrollController when the `pageToReturnTo` is null.
- feat: Added default styling and theme.
- feat: Added Iconica linter.
# 2.0.4
- feat: added maxFormWidth to AuthScreen
# 2.0.3
- feat: added default registrationOptions
# 2.0.2
- fix: fixed the issue with values not being saved when calling nextStep.
# 2.0.1
- feat: added circular progress indicator while awaiting registration of user
- feat: added alignment option for buttons
# 2.0.0
- feat(buttons): Added the possiblity to only have a next button by return zero on the previous button builder
- feat: exposed input decoration in AuthTextField
- feat: added title widget and login button builder
- feat(bool): Add a boolean field. Can be used for accepting terms and conditions
- feat(pass): Add dedicated password screen that manages state internally
- fix: Small refactor and brought back the normal alignment for the screens
- fix: Fixed alignment and spacing when opening keyboard
- feat: add auth drop down field
- fix: added step to button builders
- fix: exported auth_pass_field
- feat: added validation to disable next button
- feat(auth-screen): add flexible spacing between fields
- fix(keyboard-focus): add unfocus for onPrevious
- fix: add empty and required constructors for the RegistrationTranslations
# 1.2.0
- feat: Added the ability to have an async register function so you can call it asynchronous.

View file

@ -1,9 +1,4 @@
include: package:flutter_iconica_analysis/components_options.yaml
include: package:flutter_lints/flutter.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

48
bitbucket-pipelines.yml Normal file
View file

@ -0,0 +1,48 @@
# SPDX-FileCopyrightText: 2022 Iconica
#
# SPDX-License-Identifier: GPL-3.0-or-later
image: cirrusci/flutter
pipelines:
pull-requests:
feature/*:
- step:
caches:
- gradle
- gradlewrapper
- flutter
name: Run analyzer & test
script:
- flutter pub get
- flutter format -o none --set-exit-if-changed .
- flutter analyze
develop:
- step:
caches:
- gradle
- gradlewrapper
- flutter
name: Run analyzer & test
script:
- flutter pub get
- flutter format -o none --set-exit-if-changed .
- flutter analyze
hotfix/*:
- step:
caches:
- gradle
- gradlewrapper
- flutter
name: Run analyzer & test
script:
- flutter pub get
- flutter format -o none --set-exit-if-changed .
- flutter analyze
definitions:
caches:
gradlewrapper: ~/.gradle/wrapper
flutter: ~/.pub-cache

View file

@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
task clean(type: Delete) {
delete rootProject.buildDir
}

View file

@ -2,81 +2,27 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_registration/flutter_registration.dart';
import 'example_registration_repository.dart';
void main() {
runApp(
MaterialApp(
theme: ThemeData(
inputDecorationTheme: const InputDecorationTheme(
errorStyle: TextStyle(color: Colors.red),
),
),
home: const FlutterRegistrationDemo(),
const MaterialApp(
home: FlutterRegistrationDemo(),
),
);
}
class FlutterRegistrationDemo extends StatefulWidget {
const FlutterRegistrationDemo({super.key});
@override
State<FlutterRegistrationDemo> createState() =>
_FlutterRegistrationDemoState();
}
class _FlutterRegistrationDemoState extends State<FlutterRegistrationDemo> {
late List<AuthStep> steps;
@override
void initState() {
super.initState();
steps = RegistrationOptions.getDefaultSteps();
steps[1].fields.add(
AuthBoolField(
name: 'termsConditions',
widgetType: BoolWidgetType.checkbox,
validators: [
(value) {
if (value == null || !value) {
return 'Required';
}
return null;
},
],
rightWidget: Text.rich(
TextSpan(
text: 'I agree with the ',
children: <TextSpan>[
TextSpan(
text: 'terms & conditions',
style: const TextStyle(
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
debugPrint('Open terms and conditions');
},
),
],
),
),
),
);
}
class FlutterRegistrationDemo extends StatelessWidget {
const FlutterRegistrationDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return RegistrationScreen(
registrationOptions: RegistrationOptions(
registrationRepository: ExampleRegistrationRepository(),
registrationSteps: steps,
registrationSteps: RegistrationOptions.getDefaultSteps(),
afterRegistration: () {
debugPrint('Registered!');
},
@ -86,7 +32,8 @@ class _FlutterRegistrationDemoState extends State<FlutterRegistrationDemo> {
}
class ProtectedScreen extends StatelessWidget {
const ProtectedScreen({super.key});
const ProtectedScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Scaffold(

View file

@ -37,18 +37,18 @@ packages:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.2"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
url: "https://pub.dev"
source: hosted
version: "1.0.6"
version: "1.0.5"
fake_async:
dependency: transitive
description:
@ -62,22 +62,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_input_library:
dependency: transitive
description:
name: flutter_input_library
sha256: db8d9d57c31f166ed7c4ec3d060d18891533c57877a30c6c2668231efa1a44f8
url: "https://forgejo.internal.iconica.nl/api/packages/internal/pub/"
source: hosted
version: "3.6.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "2.0.1"
flutter_localizations:
dependency: transitive
description: flutter
@ -89,7 +81,7 @@ packages:
path: ".."
relative: true
source: path
version: "2.0.4"
version: "0.5.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -99,74 +91,50 @@ packages:
dependency: transitive
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "0.18.1"
lints:
dependency: transitive
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
sha256: "5cfd6509652ff5e7fe149b6df4859e687fca9048437857cb2e65c8d780f396e3"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.5.0"
meta:
dependency: transitive
description:
name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.9.1"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.8.3"
sky_engine:
dependency: transitive
description: flutter
@ -184,18 +152,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.11.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@ -216,10 +184,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
version: "0.6.0"
vector_math:
dependency: transitive
description:
@ -228,14 +196,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
web:
dependency: transitive
description:
name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "14.2.1"
version: "0.1.4-beta"
sdks:
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=1.17.0"

View file

@ -1,21 +1,14 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
/// Flutter registration component that provides a registration
/// screen with multiple registration steps.
library flutter_registration;
export "package:flutter_input_library/flutter_input_library.dart"
show BoolWidgetType;
export "src/config/registration_options.dart";
export "src/config/registration_translations.dart";
export "src/model/auth_bool_field.dart";
export "src/model/auth_drop_down.dart";
export "src/model/auth_exception.dart";
export "src/model/auth_field.dart";
export "src/model/auth_pass_field.dart";
export "src/model/auth_step.dart";
export "src/model/auth_text_field.dart";
export "src/registration_screen.dart";
export "src/service/registration_repository.dart";
export 'src/config/registration_options.dart';
export 'src/config/registration_translations.dart';
export 'src/model/auth_exception.dart';
export 'src/model/auth_field.dart';
export 'src/model/auth_step.dart';
export 'src/model/auth_text_field.dart';
export 'src/registration_screen.dart';
export 'src/service/registration_repository.dart';

View file

@ -2,170 +2,63 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import "dart:async";
import "dart:collection";
import "package:flutter/material.dart";
import "package:flutter_registration/flutter_registration.dart";
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:flutter_registration/flutter_registration.dart';
/// A widget for handling multi-step authentication processes.
class AuthScreen extends StatefulWidget {
/// Constructs an [AuthScreen] object.
///
/// [appBarTitle] specifies the title of the app bar.
///
/// [onFinish] is a function called upon
/// completion of the authentication process.
///
/// [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.
///
/// [titleWidget] specifies a custom widget
/// to be displayed at the top of the screen.
///
/// [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.
const AuthScreen({
required this.appBarTitle,
required this.title,
required this.steps,
required this.submitBtnTitle,
required this.nextBtnTitle,
required this.previousBtnTitle,
required this.onFinish,
this.customAppBar,
this.buttonMainAxisAlignment,
this.customBackgroundColor,
this.nextButtonBuilder,
this.previousButtonBuilder,
this.titleWidget,
this.loginButton,
this.titleFlex,
this.formFlex,
this.beforeTitleFlex,
this.afterTitleFlex,
this.maxFormWidth,
super.key,
}) : assert(steps.length > 0, "At least one step is required");
}) : assert(steps.length > 0, 'At least one step is required');
/// The title of the app bar.
final String appBarTitle;
/// A function called upon completion of the authentication process.
final String title;
final Future<void> Function({
required HashMap<String, dynamic> values,
required HashMap<String, String> values,
required void Function(int? pageToReturn) onError,
}) onFinish;
/// The authentication steps to be completed.
final List<AuthStep> steps;
/// The title of the submit button.
final String submitBtnTitle;
/// The title of the next button.
final String nextBtnTitle;
/// The title of the previous button.
final String previousBtnTitle;
/// A custom app bar widget.
final AppBar? customAppBar;
/// The alignment of the buttons.
final MainAxisAlignment? buttonMainAxisAlignment;
/// The background color of the screen.
final Color? customBackgroundColor;
/// 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;
/// A custom widget for the title.
final Widget? titleWidget;
/// A custom widget for the login button.
final Widget? loginButton;
/// The flex value for the title widget.
final int? titleFlex;
/// The flex value for the form widget.
final int? formFlex;
/// The flex value before the title widget.
final int? beforeTitleFlex;
/// The flex value after the title widget.
final int? afterTitleFlex;
/// The maximum width of the form.
final double? maxFormWidth;
final Widget Function(Future<void> Function(), String)? nextButtonBuilder;
final Widget Function(VoidCallback, String)? previousButtonBuilder;
@override
State<AuthScreen> createState() => _AuthScreenState();
}
/// The state for [AuthScreen].
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;
/// Gets the app bar.
AppBar get _appBar =>
widget.customAppBar ??
AppBar(
backgroundColor: const Color(0xffFAF9F6),
title: Text(widget.appBarTitle),
title: Text(widget.title),
);
/// Handles previous button press.
void onPrevious() {
FocusScope.of(context).unfocus();
_validate(_pageController.page!.toInt() - 1);
unawaited(
_pageController.previousPage(
duration: _animationDuration,
curve: _animationCurve,
),
_pageController.previousPage(
duration: _animationDuration,
curve: _animationCurve,
);
}
/// Handles next button press.
Future<void> onNext(AuthStep step) async {
if (!_formKey.currentState!.validate()) {
return;
@ -176,246 +69,143 @@ class _AuthScreenState extends State<AuthScreen> {
FocusScope.of(context).unfocus();
if (widget.steps.last == step) {
var values = HashMap<String, dynamic>();
var values = HashMap<String, String>();
for (var step in widget.steps) {
for (var field in step.fields) {
values[field.name] = field.value;
values[field.name] =
(field as AuthTextField).textController.value.text;
}
}
await widget.onFinish(
values: values,
onError: (int? pageToReturn) {
if (pageToReturn == null) {
return;
}
_pageController.animateToPage(
pageToReturn,
duration: _animationDuration,
curve: _animationCurve,
);
},
);
return;
} else {
_validate(_pageController.page!.toInt() + 1);
unawaited(
_pageController.nextPage(
onError: (int? pageToReturn) => _pageController.animateToPage(
pageToReturn ?? 0,
duration: _animationDuration,
curve: _animationCurve,
),
);
return;
} else {
_pageController.nextPage(
duration: _animationDuration,
curve: _animationCurve,
);
}
}
/// Validates the current step.
void _validate(int currentPage) {
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) {
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;
});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
backgroundColor: widget.customBackgroundColor ?? const Color(0xffFAF9F6),
backgroundColor: widget.customBackgroundColor ?? Colors.white,
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: [
Expanded(
flex: widget.beforeTitleFlex ?? 2,
child: Container(),
),
widget.titleWidget!,
Expanded(
flex: widget.afterTitleFlex ?? 2,
child: Container(),
),
],
body: Form(
key: _formKey,
child: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
children: <Widget>[
for (AuthStep step in widget.steps)
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Center(
child: ListView(
physics: const ClampingScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 30.0,
),
),
],
Expanded(
flex: widget.formFlex ?? 3,
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.maxFormWidth ?? 300,
),
child: Column(
children: [
for (AuthField field
in widget.steps[currentStep].fields) ...[
if (field.title != null) ...[
wrapWithDefaultStyle(
style: theme.textTheme.headlineLarge!,
widget: field.title!,
),
children: [
for (AuthField field in step.fields)
Align(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (field.title != null) ...[
field.title!,
],
field.build(),
],
field.build(context, () {
_validate(currentStep);
}),
],
],
),
),
),
)
],
),
),
Column(
),
Padding(
padding: const EdgeInsets.only(
top: 15.0,
bottom: 30.0,
left: 30.0,
right: 30.0,
),
child: Row(
mainAxisAlignment: widget.steps.first != step
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.end,
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,
if (widget.steps.first != step)
widget.previousButtonBuilder?.call(
onPrevious,
widget.previousBtnTitle,
) ??
ElevatedButton(
onPressed: onPrevious,
child: Row(
children: [
const Icon(
Icons.arrow_back,
size: 18,
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(widget.previousBtnTitle),
),
],
),
),
widget.nextButtonBuilder?.call(
() async {
await onNext(step);
},
widget.steps.last == step
? widget.submitBtnTitle
: widget.nextBtnTitle,
) ??
ElevatedButton(
onPressed: () async {
await onNext(step);
},
child: Row(
children: [
Text(
widget.steps.last == step
? widget.submitBtnTitle
: widget.nextBtnTitle,
),
const Padding(
padding: EdgeInsets.only(left: 4.0),
child: Icon(
Icons.arrow_forward,
size: 18,
),
),
],
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,
),
],
),
],
),
],
),
),
),
);
}
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,
)
],
),
),
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(
buttonText,
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
],
),
),
);
}
Widget wrapWithDefaultStyle({
required Widget widget,
required TextStyle style,
}) =>
DefaultTextStyle(style: style, child: widget);
}

View file

@ -1,12 +0,0 @@
import "dart:collection";
import "package:flutter/material.dart";
import "package:flutter_registration/flutter_registration.dart";
/// A registration repository that does nothing.
class ExampleRegistrationRepository with RegistrationRepository {
@override
Future<String?> register(HashMap values) {
debugPrint("register $values");
return Future.value(null);
}
}

View file

@ -2,227 +2,169 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import "dart:async";
import 'dart:async';
import "package:flutter/material.dart";
import "package:flutter_registration/flutter_registration.dart";
import "package:flutter_registration/src/config/example_registration_repository.dart";
import 'package:flutter/material.dart';
import 'package:flutter_registration/flutter_registration.dart';
/// A set of options for configuring the
/// registration process in a Flutter application.
class RegistrationOptions {
/// Registration options Constructor
RegistrationOptions({
required this.registrationRepository,
required this.registrationSteps,
required this.afterRegistration,
this.registrationRepository,
this.registrationSteps,
this.titleFlex,
this.formFlex,
this.beforeTitleFlex,
this.afterTitleFlex,
this.registrationTranslations = const RegistrationTranslations.empty(),
this.registrationTranslations = const RegistrationTranslations(),
this.onError,
this.customAppbarBuilder = _createCustomAppBar,
this.customAppbarBuilder,
this.nextButtonBuilder,
this.previousButtonBuilder,
this.buttonMainAxisAlignment,
this.backgroundColor,
this.titleWidget,
this.loginButton,
this.maxFormWidth,
this.beforeRegistration,
}) {
if (registrationSteps == null || registrationSteps!.isEmpty) {
steps = RegistrationOptions.getDefaultSteps();
} else {
steps = registrationSteps!;
}
registrationRepository ??= ExampleRegistrationRepository();
}
});
/// Translations for registration-related messages and prompts.
final RegistrationTranslations registrationTranslations;
/// The steps involved in the registration process.
final List<AuthStep>? registrationSteps;
/// The steps involved in the registration process.
List<AuthStep> steps = [];
/// A function that handles errors during registration.
final List<AuthStep> registrationSteps;
final int? Function(String error)? onError;
/// A callback function executed after successful registration.
final VoidCallback afterRegistration;
/// The repository responsible for registration.
RegistrationRepository? registrationRepository;
/// A function for customizing the app bar displayed during registration.
final RegistrationRepository registrationRepository;
final AppBar Function(String title)? customAppbarBuilder;
/// A function for customizing the "Next" button.
final Widget Function(
Future<void> Function()? onPressed,
String label,
int step,
// ignore: avoid_positional_boolean_parameters
bool enabled,
)? nextButtonBuilder;
/// A function for customizing the "Previous" button.
final Widget? Function(VoidCallback onPressed, String label, int step)?
final Widget Function(Future<void> Function() onPressed, String label)?
nextButtonBuilder;
final Widget Function(VoidCallback onPressed, String label)?
previousButtonBuilder;
/// Specifies the alignment of buttons.
final MainAxisAlignment? buttonMainAxisAlignment;
/// The background color of the registration screen.
final Color? backgroundColor;
/// A custom widget for displaying the registration title.
final Widget? titleWidget;
/// A custom widget for displaying a login button.
final Widget? loginButton;
/// The number of flex units for the title.
final int? titleFlex;
/// The number of flex units for the form.
final int? formFlex;
/// The number of flex units for the buttons.
final int? beforeTitleFlex;
/// The number of flex units for the buttons.
final int? afterTitleFlex;
/// The maximum width of the form. Defaults to 300.
final double? maxFormWidth;
/// This function gets called before the user gets registered.
final Future<void> Function()? beforeRegistration;
/// Generates default registration steps.
///
/// [emailController] controller for email input.
///
/// [pass1Controller] controller for first password input.
///
/// [pass1Hidden] whether the first password field is initially hidden.
///
/// [pass2Controller] controller for second password input.
///
/// [pass2Hidden] whether the second password field is initially hidden.
///
/// [passHideOnChange] function triggered when password visibility changes.
///
/// [translations] translations for default registration messages and prompts.
///
/// [titleBuilder] function for customizing step titles.
///
/// [labelBuilder] function for customizing field labels.
///
/// [textStyle] text style for input fields.
///
/// [initialEmail] initial value for email input.
static List<AuthStep> getDefaultSteps({
TextEditingController? emailController,
TextEditingController? passController,
bool passHidden = true,
// ignore: avoid_positional_boolean_parameters
TextEditingController? pass1Controller,
bool pass1Hidden = true,
TextEditingController? pass2Controller,
bool pass2Hidden = true,
Function(bool mainPass, bool value)? passHideOnChange,
RegistrationTranslations translations =
const RegistrationTranslations.empty(),
RegistrationTranslations translations = const RegistrationTranslations(),
Function(String title)? titleBuilder,
Function(String label)? labelBuilder,
TextStyle? textStyle,
TextStyle? hintStyle,
String? initialEmail,
}) =>
[
AuthStep(
fields: [
AuthTextField(
name: "email",
textEditingController: emailController,
value: initialEmail ?? "",
title: titleBuilder?.call(translations.defaultEmailTitle) ??
Padding(
padding: const EdgeInsets.only(top: 180),
child: Text(
translations.defaultEmailTitle,
),
),
textInputType: TextInputType.emailAddress,
textFieldDecoration: InputDecoration(
hintStyle: hintStyle,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
label: labelBuilder?.call(translations.defaultEmailLabel),
hintText: translations.defaultEmailHint,
border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(),
),
textStyle: textStyle,
padding: const EdgeInsets.symmetric(vertical: 20),
validators: [
// ignore: avoid_dynamic_calls
(email) => (email == null || email.isEmpty)
? translations.defaultEmailEmpty
: null,
(email) =>
RegExp(r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""")
.hasMatch(email!)
? null
: translations.defaultEmailValidatorMessage,
],
),
],
),
AuthStep(
fields: [
AuthPassField(
name: "password",
textEditingController: passController,
title: titleBuilder?.call(translations.defaultPasswordTitle) ??
Padding(
padding: const EdgeInsets.only(top: 180),
child: Text(
translations.defaultPasswordTitle,
),
),
textFieldDecoration: InputDecoration(
hintStyle: hintStyle,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
label: labelBuilder?.call(translations.defaultPasswordLabel),
hintText: translations.defaultPasswordHint,
border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(),
),
padding: const EdgeInsets.symmetric(vertical: 20),
textStyle: textStyle,
validators: [
(value) {
// ignore: avoid_dynamic_calls
if (value == null || value.isEmpty) {
return translations.defaultPasswordValidatorMessage;
}
// ignore: avoid_dynamic_calls
if (value.length < 6) {
return translations.defaultPasswordToShortValidatorMessage;
}
return null;
},
],
),
],
),
];
}
}) {
var password1 = '';
AppBar _createCustomAppBar(String title) => AppBar(
iconTheme: const IconThemeData(color: Colors.black, size: 16),
title: Text(title),
backgroundColor: Colors.transparent,
);
return [
AuthStep(
fields: [
AuthTextField(
name: 'email',
textEditingController: emailController,
value: initialEmail ?? '',
title: titleBuilder?.call(
translations.defaultEmailTitle,
) ??
Padding(
padding: const EdgeInsets.only(
top: 24.0,
bottom: 12.0,
),
child: Text(
translations.defaultEmailTitle,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
label: labelBuilder?.call(translations.defaultEmailLabel),
hintText: translations.defaultEmailHint,
textStyle: textStyle,
validators: [
(email) => (email == null || email.isEmpty)
? translations.defaultEmailEmpty
: null,
(email) =>
RegExp(r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""")
.hasMatch(email!)
? null
: translations.defaultEmailValidatorMessage,
],
)
],
),
AuthStep(
fields: [
AuthTextField(
name: 'password1',
textEditingController: pass1Controller,
title: titleBuilder?.call(
translations.defaultPassword1Title,
) ??
Padding(
padding: const EdgeInsets.only(
top: 24.0,
bottom: 12.0,
),
child: Text(
translations.defaultPassword1Title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
label: labelBuilder?.call(translations.defaultPassword1Label),
hintText: translations.defaultPassword1Hint,
textStyle: textStyle,
obscureText: true,
validators: [
(value) => (value == null || value.isEmpty)
? translations.defaultPassword1ValidatorMessage
: null,
],
onChange: (value) {
password1 = value;
},
hidden: pass1Hidden,
onPassChanged: (value) {
passHideOnChange?.call(true, value);
},
),
AuthTextField(
name: 'password2',
textEditingController: pass2Controller,
title: titleBuilder?.call(
translations.defaultPassword2Title,
) ??
Padding(
padding: const EdgeInsets.only(
top: 24.0,
bottom: 12.0,
),
child: Text(
translations.defaultPassword2Title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
label: labelBuilder?.call(translations.defaultPassword2Label),
hintText: translations.defaultPassword2Hint,
textStyle: textStyle,
obscureText: true,
validators: [
(value) {
if (pass1Controller != null) {
if (value != pass1Controller.value.text) {
return translations.defaultPassword2ValidatorMessage;
}
} else {
if (value != password1) {
return translations.defaultPassword2ValidatorMessage;
}
}
return null;
}
],
hidden: pass2Hidden,
onPassChanged: (value) {
passHideOnChange?.call(false, value);
},
),
],
),
];
}
}

View file

@ -2,129 +2,44 @@
//
// SPDX-License-Identifier: BSD-3-Clause
/// Holds all the translations for the standard elements
/// on the registration screen.
class RegistrationTranslations {
/// Constructs a [RegistrationTranslations] object.
const RegistrationTranslations({
required this.title,
required this.registerBtn,
required this.previousStepBtn,
required this.nextStepBtn,
required this.closeBtn,
required this.defaultEmailTitle,
required this.defaultEmailLabel,
required this.defaultEmailHint,
required this.defaultEmailEmpty,
required this.defaultEmailValidatorMessage,
required this.defaultPasswordTitle,
required this.defaultPasswordLabel,
required this.defaultPasswordHint,
required this.defaultPasswordValidatorMessage,
required this.defaultPasswordToShortValidatorMessage,
this.title = 'Register',
this.registerBtn = 'Register',
this.previousStepBtn = 'Previous',
this.nextStepBtn = 'Next',
this.closeBtn = 'Close',
this.defaultEmailTitle = 'What is your email?',
this.defaultEmailLabel = '',
this.defaultEmailHint = 'john.doe@domain.com',
this.defaultEmailEmpty = 'Enter your email',
this.defaultEmailValidatorMessage = 'Enter a valid email address',
this.defaultPassword1Title = 'Enter a password',
this.defaultPassword1Label = '',
this.defaultPassword1Hint = '',
this.defaultPassword1ValidatorMessage = 'Enter a valid password',
this.defaultPassword2Title = 'Re-enter password',
this.defaultPassword2Label = '',
this.defaultPassword2Hint = '',
this.defaultPassword2ValidatorMessage = 'Passwords have to be equal',
});
/// Constructs a [RegistrationTranslations] object with empty strings.
const RegistrationTranslations.empty()
: title = "",
registerBtn = "Register",
previousStepBtn = "Previous",
nextStepBtn = "Next",
closeBtn = "Close",
defaultEmailTitle = "enter your email address",
defaultEmailLabel = "",
defaultEmailHint = "Email address",
defaultEmailEmpty = "Please enter your email address.",
defaultEmailValidatorMessage = "Please enter a valid email address.",
defaultPasswordTitle = "choose a password",
defaultPasswordLabel = "",
defaultPasswordHint = "Password",
defaultPasswordValidatorMessage = "Enter a valid password",
defaultPasswordToShortValidatorMessage =
"Password needs to be at least 6 characters long";
/// The title of the registration screen.
final String title;
/// The text for the registration button.
final String registerBtn;
/// The text for the previous step button.
final String previousStepBtn;
/// The text for the next step button.
final String nextStepBtn;
/// The text for the close button.
final String closeBtn;
/// The title for the default email field.
final String defaultEmailTitle;
/// The label for the default email field.
final String defaultEmailLabel;
/// The hint for the default email field.
final String defaultEmailHint;
/// The message for an empty default email field.
final String defaultEmailEmpty;
/// The message for an invalid default email field.
final String defaultEmailValidatorMessage;
/// The title for the default password field.
final String defaultPasswordTitle;
/// The label for the default password field.
final String defaultPasswordLabel;
/// The hint for the default password field.
final String defaultPasswordHint;
/// The message for an invalid default password field.
final String defaultPasswordValidatorMessage;
/// The message for a default password that is too short.
final String defaultPasswordToShortValidatorMessage;
/// create a copywith
RegistrationTranslations copyWith({
String? title,
String? registerBtn,
String? previousStepBtn,
String? nextStepBtn,
String? closeBtn,
String? defaultEmailTitle,
String? defaultEmailLabel,
String? defaultEmailHint,
String? defaultEmailEmpty,
String? defaultEmailValidatorMessage,
String? defaultPasswordTitle,
String? defaultPasswordLabel,
String? defaultPasswordHint,
String? defaultPasswordValidatorMessage,
String? defaultPasswordToShortValidatorMessage,
}) =>
RegistrationTranslations(
title: title ?? this.title,
registerBtn: registerBtn ?? this.registerBtn,
previousStepBtn: previousStepBtn ?? this.previousStepBtn,
nextStepBtn: nextStepBtn ?? this.nextStepBtn,
closeBtn: closeBtn ?? this.closeBtn,
defaultEmailTitle: defaultEmailTitle ?? this.defaultEmailTitle,
defaultEmailLabel: defaultEmailLabel ?? this.defaultEmailLabel,
defaultEmailHint: defaultEmailHint ?? this.defaultEmailHint,
defaultEmailEmpty: defaultEmailEmpty ?? this.defaultEmailEmpty,
defaultEmailValidatorMessage:
defaultEmailValidatorMessage ?? this.defaultEmailValidatorMessage,
defaultPasswordTitle: defaultPasswordTitle ?? this.defaultPasswordTitle,
defaultPasswordLabel: defaultPasswordLabel ?? this.defaultPasswordLabel,
defaultPasswordHint: defaultPasswordHint ?? this.defaultPasswordHint,
defaultPasswordValidatorMessage: defaultPasswordValidatorMessage ??
this.defaultPasswordValidatorMessage,
defaultPasswordToShortValidatorMessage:
defaultPasswordToShortValidatorMessage ??
this.defaultPasswordToShortValidatorMessage,
);
final String defaultPassword1Title;
final String defaultPassword1Label;
final String defaultPassword1Hint;
final String defaultPassword1ValidatorMessage;
final String defaultPassword2Title;
final String defaultPassword2Label;
final String defaultPassword2Hint;
final String defaultPassword2ValidatorMessage;
}

View file

@ -2,19 +2,14 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import 'package:flutter/material.dart';
/// An action that can be performed during authentication.
class AuthAction {
/// Constructs an [AuthAction] object.
AuthAction({
required this.title,
required this.onPress,
});
/// The title of the action.
final String title;
/// A callback function triggered when the action is pressed.
final VoidCallback onPress;
}

View file

@ -1,78 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import "package:flutter_input_library/flutter_input_library.dart";
import "package:flutter_registration/flutter_registration.dart";
/// A field for capturing boolean values in a Flutter form.
///
/// Extends [AuthField].
class AuthBoolField extends AuthField {
/// Constructs an [AuthBoolField] object.
///
/// [name] specifies the name of the field.
///
/// [widgetType] defines the type of boolean widget to use.
///
/// [title] specifies the title of the field (optional).
///
/// [validators] defines a list of validation functions for the field.
///
/// [value] specifies the initial value of the field (default is false).
///
/// [leftWidget] is a widget to be displayed on the
/// left side of the boolean widget.
///
/// [rightWidget] is a widget to be displayed on the
/// right side of the boolean widget.
///
/// [onChange] is a callback function triggered when
/// the value of the field changes.
AuthBoolField({
required super.name,
required this.widgetType,
super.title,
super.validators = const [],
super.value = false,
this.leftWidget,
this.rightWidget,
this.onChange,
});
/// A widget to be displayed on the left side of the boolean widget.
final Widget? leftWidget;
/// A widget to be displayed on the right side of the boolean widget.
final Widget? rightWidget;
/// The type of boolean widget to use.
final BoolWidgetType widgetType;
/// A callback function triggered when the value of the field changes.
final Function(String value)? onChange;
@override
Widget build(BuildContext context, Function onValueChanged) =>
FlutterFormInputBool(
widgetType: widgetType,
onChanged: (v) {
value = v;
onChange?.call(value);
// ignore: avoid_dynamic_calls
onValueChanged();
},
validator: (value) {
for (var validator in validators) {
var output = validator(value);
if (output != null) {
return output;
}
}
return null;
},
leftWidget: leftWidget,
rightWidget: rightWidget,
);
}

View file

@ -1,78 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_registration/flutter_registration.dart";
/// A field for capturing dropdown selections in a Flutter form.
///
/// Extends [AuthField].
class AuthDropdownField extends AuthField {
/// Constructs an [AuthDropdownField] object.
AuthDropdownField({
required super.name,
required this.items,
required this.onChanged,
required super.value,
this.dropdownDecoration,
this.padding = const EdgeInsets.all(8.0),
this.textStyle,
this.icon = const Icon(Icons.keyboard_arrow_down),
}) {
selectedValue = value ?? items.first;
}
/// The list of items for the dropdown.
final List<String> items;
/// A callback function triggered when the dropdown value changes.
final Function(String?) onChanged;
/// The currently selected value in the dropdown.
String? selectedValue;
/// The decoration for the dropdown.
final InputDecoration? dropdownDecoration;
/// The padding around the dropdown.
final EdgeInsets padding;
/// The text style for the dropdown.
final TextStyle? textStyle;
/// The icon to be displayed with the dropdown.
final Icon icon;
@override
Widget build(BuildContext context, Function onValueChanged) => Padding(
padding: padding,
child: DropdownButtonFormField<String>(
icon: icon,
style: textStyle,
value: selectedValue,
decoration: dropdownDecoration,
items: items
.map(
(String value) => DropdownMenuItem<String>(
value: value,
child: Text(value),
),
)
.toList(),
onChanged: (newValue) {
selectedValue = newValue;
onChanged(newValue);
// ignore: avoid_dynamic_calls
onValueChanged();
},
validator: (value) {
if (validators.isNotEmpty) {
for (var validator in validators) {
var output = validator(value);
if (output != null) {
return output;
}
}
}
return null;
},
),
);
}

View file

@ -2,14 +2,12 @@
//
// SPDX-License-Identifier: BSD-3-Clause
/// An exception thrown when an authentication error occurs.
class AuthException implements Exception {
/// Constructs an [AuthException] object.
AuthException(this.message);
/// The error message.
final String message;
@override
String toString() => message;
String toString() {
return message;
}
}

View file

@ -2,53 +2,20 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import 'package:flutter/material.dart';
/// An abstract class representing a field in a Flutter form.
///
/// [T] represents the type of value stored in the field.
abstract class AuthField<T> {
/// Constructs an [AuthField] object.
///
/// [name] specifies the name of the field.
///
/// [value] specifies the initial value of the field.
///
/// [onValueChanged] is a callback function triggered when the
/// value of the field changes (optional).
///
/// [title] specifies the title widget of the field (optional).
///
/// [validators] defines a list of validation
/// functions for the field (optional).
abstract class AuthField {
AuthField({
required this.name,
required this.value,
this.onValueChanged,
this.title,
this.validators = const [],
this.value = '',
});
/// The name of the field.
final String name;
/// The initial value of the field.
T value;
/// A callback function triggered when the value of the field changes.
final Function(T)? onValueChanged;
/// The title widget of the field.
final Widget? title;
List<String? Function(String?)> validators;
String value;
/// A list of validation functions for the field.
List<String? Function(T?)> validators;
/// Builds the widget representing the field.
///
/// [context] The build context.
///
/// [onValueChanged] A function to be called when
/// the value of the field changes.
Widget build(BuildContext context, Function onValueChanged);
Widget build();
}

View file

@ -1,96 +0,0 @@
// SPDX-FileCopyrightText: 2024 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import "package:flutter_input_library/flutter_input_library.dart";
import "package:flutter_registration/flutter_registration.dart";
/// A field for capturing password inputs in a Flutter form.
///
/// Extends [AuthField].
class AuthPassField extends AuthField {
/// Constructs an [AuthPassField] object.
///
/// [name] specifies the name of the field.
///
/// [textEditingController] controller for the password input (optional).
///
/// [title] specifies the title widget of the field (optional).
///
/// [validators] defines a list of validation
/// functions for the field (optional).
///
/// [value] specifies the initial value of the
/// field (default is an empty string).
///
/// [textStyle] defines the text style for the password input.
///
/// [onChange] is a callback function triggered when
/// the value of the field changes.
///
/// [iconSize] specifies the size of the icon displayed
/// with the password input (optional).
///
/// [textFieldDecoration] defines the decoration for the
/// password input field (optional).
///
/// [padding] defines the padding around the password input
/// field (default is EdgeInsets.all(8.0)).
AuthPassField({
required super.name,
this.textEditingController,
super.title,
super.validators = const [],
super.value = "",
this.textStyle,
this.onChange,
this.iconSize,
this.textFieldDecoration,
this.padding = const EdgeInsets.all(8.0),
});
/// The text style for the password input.
final TextStyle? textStyle;
/// The size of the icon displayed with the password input.
final double? iconSize;
/// A callback function triggered when the value of the field changes.
final Function(String value)? onChange;
/// The decoration for the password input field.
final InputDecoration? textFieldDecoration;
/// The padding around the password input field.
final EdgeInsets padding;
/// The controller for the password input.
final TextEditingController? textEditingController;
@override
Widget build(BuildContext context, Function onValueChanged) => Padding(
padding: padding,
child: FlutterFormInputPassword(
style: textStyle,
iconSize: iconSize ?? 24.0,
decoration: textFieldDecoration,
onChanged: (v) {
value = v;
onChange?.call(value);
// ignore: avoid_dynamic_calls
onValueChanged();
},
validator: (value) {
for (var validator in validators) {
var output = validator(value);
if (output != null) {
return output;
}
}
return null;
},
),
);
}

View file

@ -2,15 +2,12 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_registration/src/model/auth_field.dart";
import 'package:flutter_registration/src/model/auth_field.dart';
/// A step in the authentication process.
class AuthStep {
/// Constructs an [AuthStep] object.
AuthStep({
required this.fields,
});
/// The fields in the step.
List<AuthField> fields;
}

View file

@ -2,95 +2,82 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import "package:flutter_registration/flutter_registration.dart";
import 'package:flutter/material.dart';
import 'package:flutter_registration/flutter_registration.dart';
/// A field for capturing text inputs in a Flutter form.
///
/// Extends [AuthField].
class AuthTextField extends AuthField {
/// Constructs an [AuthTextField] object.
///
/// [name] specifies the name of the field.
///
/// [textEditingController] controller for the text input (optional).
///
/// [title] specifies the title widget of the field (optional).
///
/// [validators] defines a list of validation
/// functions for the field (optional).
///
/// [value] specifies the initial value of the
/// field (default is an empty string).
///
/// [textStyle] defines the text style for the text input.
///
/// [onChange] is a callback function triggered
/// when the value of the field changes.
///
/// [textFieldDecoration] defines the decoration
/// for the text input field (optional).
///
/// [padding] defines the padding around the text
/// input field (default is EdgeInsets.all(8.0)).
AuthTextField({
required super.name,
TextEditingController? textEditingController,
super.title,
super.validators = const [],
super.value = "",
super.value = '',
this.obscureText = false,
this.hintText,
this.label,
this.textStyle,
this.onChange,
this.textFieldDecoration,
this.padding = const EdgeInsets.all(8.0),
this.textInputType,
this.hidden,
this.onPassChanged,
}) {
textController =
textEditingController ?? TextEditingController(text: value);
}
/// The controller for the text input.
late TextEditingController textController;
/// The text style for the text input.
final bool obscureText;
final String? hintText;
final Widget? label;
final TextStyle? textStyle;
/// A callback function triggered when the value of the field changes.
final Function(String value)? onChange;
/// The decoration for the text input field.
final InputDecoration? textFieldDecoration;
/// The padding around the text input field.
final EdgeInsets padding;
/// The type of text input.
final TextInputType? textInputType;
final bool? hidden;
final Function(bool value)? onPassChanged;
@override
Widget build(BuildContext context, Function onValueChanged) => Padding(
padding: padding,
child: TextFormField(
keyboardType: textInputType,
style: textStyle,
decoration: textFieldDecoration,
controller: textController,
onChanged: (v) {
value = v;
onChange?.call(value);
// ignore: avoid_dynamic_calls
onValueChanged();
},
validator: (value) {
for (var validator in validators) {
var output = validator(value);
if (output != null) {
return output;
}
}
Widget build() {
Widget? suffix;
return null;
if (hidden != null) {
if (hidden!) {
suffix = GestureDetector(
onTap: () {
onPassChanged?.call(!hidden!);
},
),
);
child: const Icon(Icons.visibility),
);
} else {
suffix = GestureDetector(
onTap: () {
onPassChanged?.call(!hidden!);
},
child: const Icon(Icons.visibility_off),
);
}
}
return TextFormField(
style: textStyle,
decoration: InputDecoration(
label: label,
hintText: hintText,
suffix: suffix,
),
controller: textController,
obscureText: hidden ?? obscureText,
onChanged: (v) {
value = v;
onChange?.call(value);
},
validator: (value) {
for (var validator in validators) {
var output = validator(value);
if (output != null) {
return output;
}
}
return null;
},
);
}
}

View file

@ -1,77 +1,58 @@
import "dart:collection";
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import "package:flutter_registration/flutter_registration.dart";
import "package:flutter_registration/src/auth_screen.dart";
import 'dart:collection';
/// A screen for user registration.
class RegistrationScreen extends StatefulWidget {
/// Constructs a [RegistrationScreen] object.
///
/// [registrationOptions] specifies the registration options.
import 'package:flutter/material.dart';
import 'package:flutter_registration/flutter_registration.dart';
import 'package:flutter_registration/src/auth_screen.dart';
class RegistrationScreen extends StatelessWidget {
const RegistrationScreen({
required this.registrationOptions,
super.key,
});
/// The registration options.
final RegistrationOptions registrationOptions;
@override
RegistrationScreenState createState() => RegistrationScreenState();
}
/// The state for [RegistrationScreen].
class RegistrationScreenState extends State<RegistrationScreen> {
/// Registers the user.
Future<void> _register({
required HashMap<String, dynamic> values,
Future<void> register({
required HashMap<String, String> values,
required void Function(int? pageToReturn) onError,
}) async {
try {
await widget.registrationOptions.beforeRegistration?.call();
var registered = await widget.registrationOptions.registrationRepository!
.register(values);
var registered =
await registrationOptions.registrationRepository.register(values);
if (registered == null) {
widget.registrationOptions.afterRegistration();
registrationOptions.afterRegistration();
} else {
var pageToReturn = widget.registrationOptions.onError?.call(registered);
var pageToReturn = registrationOptions.onError?.call(registered);
onError(pageToReturn);
}
} on Exception catch (_) {
onError(null);
} catch (e) {
onError(0);
}
}
@override
Widget build(BuildContext context) {
var translations = widget.registrationOptions.registrationTranslations;
var translations = registrationOptions.registrationTranslations;
return AuthScreen(
steps: widget.registrationOptions.steps,
customAppBar: widget.registrationOptions.customAppbarBuilder?.call(
steps: registrationOptions.registrationSteps,
customAppBar: registrationOptions.customAppbarBuilder?.call(
translations.title,
),
onFinish: _register,
appBarTitle: translations.title,
onFinish: register,
title: translations.title,
submitBtnTitle: translations.registerBtn,
nextBtnTitle: translations.nextStepBtn,
previousBtnTitle: translations.previousStepBtn,
nextButtonBuilder: widget.registrationOptions.nextButtonBuilder,
previousButtonBuilder: widget.registrationOptions.previousButtonBuilder,
buttonMainAxisAlignment:
widget.registrationOptions.buttonMainAxisAlignment,
customBackgroundColor: widget.registrationOptions.backgroundColor,
titleWidget: widget.registrationOptions.titleWidget,
loginButton: widget.registrationOptions.loginButton,
titleFlex: widget.registrationOptions.titleFlex,
formFlex: widget.registrationOptions.formFlex,
beforeTitleFlex: widget.registrationOptions.beforeTitleFlex,
afterTitleFlex: widget.registrationOptions.afterTitleFlex,
maxFormWidth: widget.registrationOptions.maxFormWidth,
nextButtonBuilder: registrationOptions.nextButtonBuilder,
previousButtonBuilder: registrationOptions.previousButtonBuilder,
customBackgroundColor: registrationOptions.backgroundColor,
);
}
}

View file

@ -2,10 +2,8 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import "dart:collection";
import 'dart:collection';
/// A mixin for a registration repository.
mixin RegistrationRepository {
/// Registers a user with the given values.
Future<String?> register(HashMap values);
}

View file

@ -4,20 +4,14 @@
name: flutter_registration
description: A Flutter Registration package
version: 3.0.0
version: 1.2.0
repository: https://github.com/Iconica-Development/flutter_registration
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
environment:
sdk: ">=3.0.0 <4.0.0"
sdk: ">=2.18.0 <3.0.0"
flutter: ">=1.17.0"
dependencies:
flutter_input_library:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^3.6.0
flutter:
sdk: flutter
flutter_localizations:
@ -26,9 +20,6 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter_lints: ^2.0.0
flutter:

View file

@ -3,10 +3,10 @@
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-License-Identifier: GPL-3.0-or-later
import "package:flutter_test/flutter_test.dart";
import 'package:flutter_test/flutter_test.dart';
void main() {
test("test", () {
test('test', () {
expect(true, true);
});
}