feat: default styling

This commit is contained in:
mike doornenbal 2024-08-08 10:57:20 +02:00
parent cf6e30abec
commit 4bad032588
21 changed files with 671 additions and 521 deletions

View file

@ -3,7 +3,10 @@ SPDX-FileCopyrightText: 2022 Iconica
SPDX-License-Identifier: GPL-3.0-or-later 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 # 2.0.4
- feat: added maxFormWidth to AuthScreen - feat: added maxFormWidth to AuthScreen

View file

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

View file

@ -21,7 +21,7 @@ void main() {
} }
class FlutterRegistrationDemo extends StatefulWidget { class FlutterRegistrationDemo extends StatefulWidget {
const FlutterRegistrationDemo({Key? key}) : super(key: key); const FlutterRegistrationDemo({super.key});
@override @override
State<FlutterRegistrationDemo> createState() => State<FlutterRegistrationDemo> createState() =>
@ -86,8 +86,7 @@ class _FlutterRegistrationDemoState extends State<FlutterRegistrationDemo> {
} }
class ProtectedScreen extends StatelessWidget { class ProtectedScreen extends StatelessWidget {
const ProtectedScreen({Key? key}) : super(key: key); const ProtectedScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold( return const Scaffold(

View file

@ -66,10 +66,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_input_library name: flutter_input_library
sha256: "346823caa633d47ad21d8ce92f3c2ed460619fd5447999c35cb171da5e86af06" sha256: db8d9d57c31f166ed7c4ec3d060d18891533c57877a30c6c2668231efa1a44f8
url: "https://forgejo.internal.iconica.nl/api/packages/internal/pub/" url: "https://forgejo.internal.iconica.nl/api/packages/internal/pub/"
source: hosted source: hosted
version: "3.3.1" version: "3.6.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:

View file

@ -1,19 +1,21 @@
// SPDX-FileCopyrightText: 2022 Iconica // SPDX-FileCopyrightText: 2022 Iconica
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
/// Flutter registration component that provides a registration screen with multiple registration steps. /// Flutter registration component that provides a registration
/// screen with multiple registration steps.
library flutter_registration; library flutter_registration;
export 'src/config/registration_options.dart'; export "package:flutter_input_library/flutter_input_library.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/model/auth_bool_field.dart';
export 'src/model/auth_drop_down.dart';
export 'src/model/auth_pass_field.dart';
export 'src/registration_screen.dart';
export 'src/service/registration_repository.dart';
export 'package:flutter_input_library/flutter_input_library.dart'
show BoolWidgetType; 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";

View file

@ -2,10 +2,10 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'dart:async'; import "dart:async";
import 'dart:collection'; import "dart:collection";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_registration/flutter_registration.dart'; import "package:flutter_registration/flutter_registration.dart";
/// A widget for handling multi-step authentication processes. /// A widget for handling multi-step authentication processes.
class AuthScreen extends StatefulWidget { class AuthScreen extends StatefulWidget {
@ -13,7 +13,8 @@ class AuthScreen extends StatefulWidget {
/// ///
/// [appBarTitle] specifies the title of the app bar. /// [appBarTitle] specifies the title of the app bar.
/// ///
/// [onFinish] is a function called upon completion of the authentication process. /// [onFinish] is a function called upon
/// completion of the authentication process.
/// ///
/// [steps] is a list of authentication steps to be completed. /// [steps] is a list of authentication steps to be completed.
/// ///
@ -33,7 +34,8 @@ class AuthScreen extends StatefulWidget {
/// ///
/// [previousButtonBuilder] allows customization of the previous button. /// [previousButtonBuilder] allows customization of the previous button.
/// ///
/// [titleWidget] specifies a custom widget to be displayed at the top of the screen. /// [titleWidget] specifies a custom widget
/// to be displayed at the top of the screen.
/// ///
/// [loginButton] specifies a custom login button widget. /// [loginButton] specifies a custom login button widget.
/// ///
@ -44,8 +46,7 @@ class AuthScreen extends StatefulWidget {
/// [beforeTitleFlex] specifies the flex value before the title widget. /// [beforeTitleFlex] specifies the flex value before the title widget.
/// ///
/// [afterTitleFlex] specifies the flex value after the title widget. /// [afterTitleFlex] specifies the flex value after the title widget.
///
/// [isLoading] indicates whether the screen is in a loading state.
const AuthScreen({ const AuthScreen({
required this.appBarTitle, required this.appBarTitle,
required this.steps, required this.steps,
@ -64,35 +65,72 @@ class AuthScreen extends StatefulWidget {
this.formFlex, this.formFlex,
this.beforeTitleFlex, this.beforeTitleFlex,
this.afterTitleFlex, this.afterTitleFlex,
this.isLoading = false,
this.maxFormWidth, this.maxFormWidth,
Key? key, super.key,
}) : assert(steps.length > 0, 'At least one step is required'), }) : assert(steps.length > 0, "At least one step is required");
super(key: key);
/// The title of the app bar.
final String appBarTitle; final String appBarTitle;
/// A function called upon completion of the authentication process.
final Future<void> Function({ final Future<void> Function({
required HashMap<String, dynamic> values, required HashMap<String, dynamic> values,
required void Function(int? pageToReturn) onError, required void Function(int? pageToReturn) onError,
}) onFinish; }) onFinish;
/// The authentication steps to be completed.
final List<AuthStep> steps; final List<AuthStep> steps;
/// The title of the submit button.
final String submitBtnTitle; final String submitBtnTitle;
/// The title of the next button.
final String nextBtnTitle; final String nextBtnTitle;
/// The title of the previous button.
final String previousBtnTitle; final String previousBtnTitle;
/// A custom app bar widget.
final AppBar? customAppBar; final AppBar? customAppBar;
/// The alignment of the buttons.
final MainAxisAlignment? buttonMainAxisAlignment; final MainAxisAlignment? buttonMainAxisAlignment;
/// The background color of the screen.
final Color? customBackgroundColor; final Color? customBackgroundColor;
final Widget Function(Future<void> Function()? onPressed, String label,
int step, bool enabled)? nextButtonBuilder; /// 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)? final Widget? Function(VoidCallback onPressed, String label, int step)?
previousButtonBuilder; previousButtonBuilder;
/// A custom widget for the title.
final Widget? titleWidget; final Widget? titleWidget;
/// A custom widget for the login button.
final Widget? loginButton; final Widget? loginButton;
/// The flex value for the title widget.
final int? titleFlex; final int? titleFlex;
/// The flex value for the form widget.
final int? formFlex; final int? formFlex;
/// The flex value before the title widget.
final int? beforeTitleFlex; final int? beforeTitleFlex;
/// The flex value after the title widget.
final int? afterTitleFlex; final int? afterTitleFlex;
final bool isLoading;
/// The maximum width of the form.
final double? maxFormWidth; final double? maxFormWidth;
@override @override
@ -119,9 +157,11 @@ class _AuthScreenState extends State<AuthScreen> {
void onPrevious() { void onPrevious() {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
_validate(_pageController.page!.toInt() - 1); _validate(_pageController.page!.toInt() - 1);
unawaited(
_pageController.previousPage( _pageController.previousPage(
duration: _animationDuration, duration: _animationDuration,
curve: _animationCurve, curve: _animationCurve,
),
); );
} }
@ -146,31 +186,38 @@ class _AuthScreenState extends State<AuthScreen> {
await widget.onFinish( await widget.onFinish(
values: values, values: values,
onError: (int? pageToReturn) => _pageController.animateToPage( onError: (int? pageToReturn) {
pageToReturn ?? 0, if (pageToReturn == null) {
return;
}
_pageController.animateToPage(
pageToReturn,
duration: _animationDuration, duration: _animationDuration,
curve: _animationCurve, curve: _animationCurve,
), );
},
); );
return; return;
} else { } else {
_validate(_pageController.page!.toInt() + 1); _validate(_pageController.page!.toInt() + 1);
unawaited(
_pageController.nextPage( _pageController.nextPage(
duration: _animationDuration, duration: _animationDuration,
curve: _animationCurve, curve: _animationCurve,
),
); );
} }
} }
/// Validates the current step. /// Validates the current step.
void _validate(int currentPage) { void _validate(int currentPage) {
bool isStepValid = true; var isStepValid = true;
// Loop through each field in the current step // Loop through each field in the current step
for (var field in widget.steps[currentPage].fields) { for (var field in widget.steps[currentPage].fields) {
for (var validator in field.validators) { for (var validator in field.validators) {
String? validationResult = validator(field.value); var validationResult = validator(field.value);
if (validationResult != null) { if (validationResult != null) {
// If any validator returns an error, mark step as invalid and break // If any validator returns an error, mark step as invalid and break
isStepValid = false; isStepValid = false;
@ -189,17 +236,9 @@ class _AuthScreenState extends State<AuthScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.isLoading var theme = Theme.of(context);
? const Center( return Scaffold(
child: SizedBox( backgroundColor: widget.customBackgroundColor ?? const Color(0xffFAF9F6),
height: 120,
width: 120,
child: CircularProgressIndicator(),
),
)
: Scaffold(
backgroundColor:
widget.customBackgroundColor ?? const Color(0xffFAF9F6),
appBar: _appBar, appBar: _appBar,
body: SafeArea( body: SafeArea(
child: Form( child: Form(
@ -208,7 +247,9 @@ class _AuthScreenState extends State<AuthScreen> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
controller: _pageController, controller: _pageController,
children: <Widget>[ children: <Widget>[
for (var i = 0; i < widget.steps.length; i++) for (var currentStep = 0;
currentStep < widget.steps.length;
currentStep++)
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -244,14 +285,17 @@ class _AuthScreenState extends State<AuthScreen> {
child: Column( child: Column(
children: [ children: [
for (AuthField field for (AuthField field
in widget.steps[i].fields) ...[ in widget.steps[currentStep].fields) ...[
if (field.title != null) ...[ if (field.title != null) ...[
field.title!, wrapWithDefaultStyle(
style: theme.textTheme.headlineLarge!,
widget: field.title!,
),
], ],
field.build(context, () { field.build(context, () {
_validate(i); _validate(currentStep);
}) }),
] ],
], ],
), ),
), ),
@ -259,124 +303,70 @@ class _AuthScreenState extends State<AuthScreen> {
), ),
Column( Column(
children: [ children: [
Row( SizedBox(
mainAxisAlignment: widget width: MediaQuery.of(context).size.width,
.buttonMainAxisAlignment != child: Padding(
null padding: const EdgeInsets.symmetric(
? widget.buttonMainAxisAlignment! horizontal: 20,
: (widget.previousButtonBuilder != null && ),
child: Row(
mainAxisAlignment: widget.steps.first !=
widget.steps[currentStep]
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.end,
children: [
if (widget.steps.first !=
widget.steps[currentStep]) ...[
widget.previousButtonBuilder?.call( widget.previousButtonBuilder?.call(
onPrevious, onPrevious,
widget.previousBtnTitle, widget.previousBtnTitle,
i, currentStep,
) == ) ??
null) _stepButton(
? MainAxisAlignment.start buttonText: widget.previousBtnTitle,
: widget.steps.first != widget.steps[i] onTap: () async => onPrevious(),
? MainAxisAlignment.center
: MainAxisAlignment.end,
children: [
if (widget.previousButtonBuilder == null) ...[
if (widget.steps.first != widget.steps[i])
Padding(
padding: const EdgeInsets.only(
left: 16, bottom: 10, right: 8),
child: InkWell(
onTap: onPrevious,
child: Container(
width: 180,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(20),
border: Border.all(
color: const Color(
0xff979797,
), ),
const SizedBox(
width: 8,
), ),
),
child: Center(
child: Padding(
padding:
const EdgeInsets.symmetric(
vertical: 2.0),
child: Text(
widget.previousBtnTitle,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
),
] else if (widget.previousButtonBuilder?.call(
onPrevious,
widget.previousBtnTitle,
i) !=
null) ...[
widget.previousButtonBuilder!.call(
onPrevious, widget.previousBtnTitle, i)!
], ],
widget.nextButtonBuilder?.call( widget.nextButtonBuilder?.call(
!_formValid !_formValid
? null ? null
: () async { : () async {
await onNext(widget.steps[i]); await onNext(
widget.steps[currentStep],
);
}, },
widget.steps.last == widget.steps[i] widget.steps.last ==
widget.steps[currentStep]
? widget.submitBtnTitle ? widget.submitBtnTitle
: widget.nextBtnTitle, : widget.nextBtnTitle,
i, currentStep,
_formValid, _formValid,
) ?? ) ??
Padding( _stepButton(
padding: const EdgeInsets.only( buttonText: widget.steps.last ==
right: 16, bottom: 10, left: 8), widget.steps[currentStep]
child: InkWell(
onTap: !_formValid
? null
: () async {
await onNext(widget.steps[i]);
},
child: Container(
width: 180,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(20),
border: Border.all(
color: const Color(
0xff979797,
),
),
),
child: Center(
child: Padding(
padding:
const EdgeInsets.symmetric(
vertical: 2.0),
child: Text(
widget.steps.last ==
widget.steps[i]
? widget.submitBtnTitle ? widget.submitBtnTitle
: widget.nextBtnTitle, : widget.nextBtnTitle,
style: const TextStyle( onTap: () async {
fontSize: 16, await onNext(
fontWeight: FontWeight.w500, widget.steps[currentStep],
), );
), },
),
),
),
),
), ),
], ],
), ),
),
),
const SizedBox(
height: 8,
),
if (widget.loginButton != null) if (widget.loginButton != null)
Padding( Padding(
padding: const EdgeInsets.only(top: 20.0), padding: const EdgeInsets.only(top: 20.0),
child: widget.loginButton!, child: widget.loginButton,
), ),
], ],
), ),
@ -388,4 +378,44 @@ class _AuthScreenState extends State<AuthScreen> {
), ),
); );
} }
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,11 +1,12 @@
import 'dart:collection'; import "dart:collection";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_registration/flutter_registration.dart'; import "package:flutter_registration/flutter_registration.dart";
/// A registration repository that does nothing.
class ExampleRegistrationRepository with RegistrationRepository { class ExampleRegistrationRepository with RegistrationRepository {
@override @override
Future<String?> register(HashMap values) { Future<String?> register(HashMap values) {
debugPrint('register $values'); debugPrint("register $values");
return Future.value(null); return Future.value(null);
} }
} }

View file

@ -2,18 +2,20 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'dart:async'; import "dart:async";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_registration/flutter_registration.dart'; import "package:flutter_registration/flutter_registration.dart";
import 'package:flutter_registration/src/config/example_registration_repository.dart'; import "package:flutter_registration/src/config/example_registration_repository.dart";
/// A set of options for configuring the registration process in a Flutter application. /// A set of options for configuring the
/// registration process in a Flutter application.
class RegistrationOptions { class RegistrationOptions {
/// Registration options Constructor
RegistrationOptions({ RegistrationOptions({
required this.afterRegistration,
this.registrationRepository, this.registrationRepository,
this.registrationSteps, this.registrationSteps,
required this.afterRegistration,
this.titleFlex, this.titleFlex,
this.formFlex, this.formFlex,
this.beforeTitleFlex, this.beforeTitleFlex,
@ -28,6 +30,7 @@ class RegistrationOptions {
this.titleWidget, this.titleWidget,
this.loginButton, this.loginButton,
this.maxFormWidth, this.maxFormWidth,
this.beforeRegistration,
}) { }) {
if (registrationSteps == null || registrationSteps!.isEmpty) { if (registrationSteps == null || registrationSteps!.isEmpty) {
steps = RegistrationOptions.getDefaultSteps(); steps = RegistrationOptions.getDefaultSteps();
@ -43,6 +46,7 @@ class RegistrationOptions {
/// The steps involved in the registration process. /// The steps involved in the registration process.
final List<AuthStep>? registrationSteps; final List<AuthStep>? registrationSteps;
/// The steps involved in the registration process.
List<AuthStep> steps = []; List<AuthStep> steps = [];
/// A function that handles errors during registration. /// A function that handles errors during registration.
@ -58,8 +62,13 @@ class RegistrationOptions {
final AppBar Function(String title)? customAppbarBuilder; final AppBar Function(String title)? customAppbarBuilder;
/// A function for customizing the "Next" button. /// A function for customizing the "Next" button.
final Widget Function(Future<void> Function()? onPressed, String label, final Widget Function(
int step, bool enabled)? nextButtonBuilder; Future<void> Function()? onPressed,
String label,
int step,
// ignore: avoid_positional_boolean_parameters
bool enabled,
)? nextButtonBuilder;
/// A function for customizing the "Previous" button. /// A function for customizing the "Previous" button.
final Widget? Function(VoidCallback onPressed, String label, int step)? final Widget? Function(VoidCallback onPressed, String label, int step)?
@ -92,6 +101,9 @@ class RegistrationOptions {
/// The maximum width of the form. Defaults to 300. /// The maximum width of the form. Defaults to 300.
final double? maxFormWidth; final double? maxFormWidth;
/// This function gets called before the user gets registered.
final Future<void> Function()? beforeRegistration;
/// Generates default registration steps. /// Generates default registration steps.
/// ///
/// [emailController] controller for email input. /// [emailController] controller for email input.
@ -119,42 +131,43 @@ class RegistrationOptions {
TextEditingController? emailController, TextEditingController? emailController,
TextEditingController? passController, TextEditingController? passController,
bool passHidden = true, bool passHidden = true,
// ignore: avoid_positional_boolean_parameters
Function(bool mainPass, bool value)? passHideOnChange, Function(bool mainPass, bool value)? passHideOnChange,
RegistrationTranslations translations = RegistrationTranslations translations =
const RegistrationTranslations.empty(), const RegistrationTranslations.empty(),
Function(String title)? titleBuilder, Function(String title)? titleBuilder,
Function(String label)? labelBuilder, Function(String label)? labelBuilder,
TextStyle? textStyle, TextStyle? textStyle,
TextStyle? hintStyle,
String? initialEmail, String? initialEmail,
}) { }) =>
return [ [
AuthStep( AuthStep(
fields: [ fields: [
AuthTextField( AuthTextField(
name: 'email', name: "email",
textEditingController: emailController, textEditingController: emailController,
value: initialEmail ?? '', value: initialEmail ?? "",
title: titleBuilder?.call(translations.defaultEmailTitle) ?? title: titleBuilder?.call(translations.defaultEmailTitle) ??
const Padding( Padding(
padding: EdgeInsets.only(top: 180), padding: const EdgeInsets.only(top: 180),
child: Text( child: Text(
'Enter your e-mail', translations.defaultEmailTitle,
style: TextStyle(
color: Color(0xff71C6D1),
fontWeight: FontWeight.w800,
fontSize: 24,
),
), ),
), ),
textInputType: TextInputType.emailAddress,
textFieldDecoration: InputDecoration( textFieldDecoration: InputDecoration(
hintStyle: hintStyle,
contentPadding: const EdgeInsets.symmetric(horizontal: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 8),
label: labelBuilder?.call(translations.defaultEmailLabel), label: labelBuilder?.call(translations.defaultEmailLabel),
hintText: translations.defaultEmailHint, hintText: translations.defaultEmailHint,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(),
), ),
textStyle: textStyle, textStyle: textStyle,
padding: const EdgeInsets.symmetric(vertical: 20), padding: const EdgeInsets.symmetric(vertical: 20),
validators: [ validators: [
// ignore: avoid_dynamic_calls
(email) => (email == null || email.isEmpty) (email) => (email == null || email.isEmpty)
? translations.defaultEmailEmpty ? translations.defaultEmailEmpty
: null, : null,
@ -170,43 +183,46 @@ class RegistrationOptions {
AuthStep( AuthStep(
fields: [ fields: [
AuthPassField( AuthPassField(
name: 'password', name: "password",
textEditingController: passController, textEditingController: passController,
title: titleBuilder?.call(translations.defaultPasswordTitle) ?? title: titleBuilder?.call(translations.defaultPasswordTitle) ??
Padding( Padding(
padding: const EdgeInsets.only(top: 180), padding: const EdgeInsets.only(top: 180),
child: Text( child: Text(
translations.defaultPasswordTitle, translations.defaultPasswordTitle,
style: const TextStyle(
color: Color(0xff71C6D1),
fontWeight: FontWeight.w800,
fontSize: 24,
),
), ),
), ),
textFieldDecoration: InputDecoration( textFieldDecoration: InputDecoration(
hintStyle: hintStyle,
contentPadding: const EdgeInsets.symmetric(horizontal: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 8),
label: labelBuilder?.call(translations.defaultPasswordLabel), label: labelBuilder?.call(translations.defaultPasswordLabel),
hintText: translations.defaultPasswordHint, hintText: translations.defaultPasswordHint,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(),
), ),
padding: const EdgeInsets.symmetric(vertical: 20), padding: const EdgeInsets.symmetric(vertical: 20),
textStyle: textStyle, textStyle: textStyle,
validators: [ validators: [
(value) => (value == null || value.isEmpty) (value) {
? translations.defaultPasswordValidatorMessage // ignore: avoid_dynamic_calls
: null, if (value == null || value.isEmpty) {
return translations.defaultPasswordValidatorMessage;
}
// ignore: avoid_dynamic_calls
if (value.length < 6) {
return translations.defaultPasswordToShortValidatorMessage;
}
return null;
},
], ],
), ),
], ],
), ),
]; ];
}
} }
AppBar _createCustomAppBar(String title) { AppBar _createCustomAppBar(String title) => AppBar(
return AppBar( iconTheme: const IconThemeData(color: Colors.black, size: 16),
title: Text(title), title: Text(title),
backgroundColor: const Color(0xffFAF9F6), backgroundColor: Colors.transparent,
); );
}

View file

@ -2,8 +2,10 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
/// Holds all the translations for the standard elements on the registration screen. /// Holds all the translations for the standard elements
/// on the registration screen.
class RegistrationTranslations { class RegistrationTranslations {
/// Constructs a [RegistrationTranslations] object.
const RegistrationTranslations({ const RegistrationTranslations({
required this.title, required this.title,
required this.registerBtn, required this.registerBtn,
@ -19,40 +21,74 @@ class RegistrationTranslations {
required this.defaultPasswordLabel, required this.defaultPasswordLabel,
required this.defaultPasswordHint, required this.defaultPasswordHint,
required this.defaultPasswordValidatorMessage, required this.defaultPasswordValidatorMessage,
required this.defaultPasswordToShortValidatorMessage,
}); });
/// Constructs a [RegistrationTranslations] object with empty strings.
const RegistrationTranslations.empty() const RegistrationTranslations.empty()
: title = '', : title = "",
registerBtn = 'Register', registerBtn = "Register",
previousStepBtn = 'Previous', previousStepBtn = "Previous",
nextStepBtn = 'Next', nextStepBtn = "Next",
closeBtn = 'Close', closeBtn = "Close",
defaultEmailTitle = 'What is your email?', defaultEmailTitle = "enter your email address",
defaultEmailLabel = '', defaultEmailLabel = "",
defaultEmailHint = 'Email address', defaultEmailHint = "Email address",
defaultEmailEmpty = 'Please enter your email address.', defaultEmailEmpty = "Please enter your email address.",
defaultEmailValidatorMessage = 'Please enter a valid email address.', defaultEmailValidatorMessage = "Please enter a valid email address.",
defaultPasswordTitle = 'Choose a password', defaultPasswordTitle = "choose a password",
defaultPasswordLabel = 'password', defaultPasswordLabel = "",
defaultPasswordHint = '', defaultPasswordHint = "Password",
defaultPasswordValidatorMessage = 'Enter a valid 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; final String title;
/// The text for the registration button.
final String registerBtn; final String registerBtn;
/// The text for the previous step button.
final String previousStepBtn; final String previousStepBtn;
/// The text for the next step button.
final String nextStepBtn; final String nextStepBtn;
/// The text for the close button.
final String closeBtn; final String closeBtn;
/// The title for the default email field.
final String defaultEmailTitle; final String defaultEmailTitle;
/// The label for the default email field.
final String defaultEmailLabel; final String defaultEmailLabel;
/// The hint for the default email field.
final String defaultEmailHint; final String defaultEmailHint;
/// The message for an empty default email field.
final String defaultEmailEmpty; final String defaultEmailEmpty;
/// The message for an invalid default email field.
final String defaultEmailValidatorMessage; final String defaultEmailValidatorMessage;
/// The title for the default password field.
final String defaultPasswordTitle; final String defaultPasswordTitle;
/// The label for the default password field.
final String defaultPasswordLabel; final String defaultPasswordLabel;
/// The hint for the default password field.
final String defaultPasswordHint; final String defaultPasswordHint;
/// The message for an invalid default password field.
final String defaultPasswordValidatorMessage; final String defaultPasswordValidatorMessage;
// create a copywith /// The message for a default password that is too short.
final String defaultPasswordToShortValidatorMessage;
/// create a copywith
RegistrationTranslations copyWith({ RegistrationTranslations copyWith({
String? title, String? title,
String? registerBtn, String? registerBtn,
@ -68,8 +104,9 @@ class RegistrationTranslations {
String? defaultPasswordLabel, String? defaultPasswordLabel,
String? defaultPasswordHint, String? defaultPasswordHint,
String? defaultPasswordValidatorMessage, String? defaultPasswordValidatorMessage,
}) { String? defaultPasswordToShortValidatorMessage,
return RegistrationTranslations( }) =>
RegistrationTranslations(
title: title ?? this.title, title: title ?? this.title,
registerBtn: registerBtn ?? this.registerBtn, registerBtn: registerBtn ?? this.registerBtn,
previousStepBtn: previousStepBtn ?? this.previousStepBtn, previousStepBtn: previousStepBtn ?? this.previousStepBtn,
@ -86,6 +123,8 @@ class RegistrationTranslations {
defaultPasswordHint: defaultPasswordHint ?? this.defaultPasswordHint, defaultPasswordHint: defaultPasswordHint ?? this.defaultPasswordHint,
defaultPasswordValidatorMessage: defaultPasswordValidatorMessage ?? defaultPasswordValidatorMessage: defaultPasswordValidatorMessage ??
this.defaultPasswordValidatorMessage, this.defaultPasswordValidatorMessage,
defaultPasswordToShortValidatorMessage:
defaultPasswordToShortValidatorMessage ??
this.defaultPasswordToShortValidatorMessage,
); );
}
} }

View file

@ -2,14 +2,19 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // 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 { class AuthAction {
/// Constructs an [AuthAction] object.
AuthAction({ AuthAction({
required this.title, required this.title,
required this.onPress, required this.onPress,
}); });
/// The title of the action.
final String title; final String title;
/// A callback function triggered when the action is pressed.
final VoidCallback onPress; final VoidCallback onPress;
} }

View file

@ -2,9 +2,9 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_input_library/flutter_input_library.dart'; import "package:flutter_input_library/flutter_input_library.dart";
import 'package:flutter_registration/flutter_registration.dart'; import "package:flutter_registration/flutter_registration.dart";
/// A field for capturing boolean values in a Flutter form. /// A field for capturing boolean values in a Flutter form.
/// ///
@ -22,11 +22,14 @@ class AuthBoolField extends AuthField {
/// ///
/// [value] specifies the initial value of the field (default is false). /// [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. /// [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. /// [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. /// [onChange] is a callback function triggered when
/// the value of the field changes.
AuthBoolField({ AuthBoolField({
required super.name, required super.name,
required this.widgetType, required this.widgetType,
@ -38,18 +41,26 @@ class AuthBoolField extends AuthField {
this.onChange, this.onChange,
}); });
/// A widget to be displayed on the left side of the boolean widget.
final Widget? leftWidget; final Widget? leftWidget;
/// A widget to be displayed on the right side of the boolean widget.
final Widget? rightWidget; final Widget? rightWidget;
/// The type of boolean widget to use.
final BoolWidgetType widgetType; final BoolWidgetType widgetType;
/// A callback function triggered when the value of the field changes.
final Function(String value)? onChange; final Function(String value)? onChange;
@override @override
Widget build(BuildContext context, Function onValueChanged) { Widget build(BuildContext context, Function onValueChanged) =>
return FlutterFormInputBool( FlutterFormInputBool(
widgetType: widgetType, widgetType: widgetType,
onChanged: (v) { onChanged: (v) {
value = v; value = v;
onChange?.call(value); onChange?.call(value);
// ignore: avoid_dynamic_calls
onValueChanged(); onValueChanged();
}, },
validator: (value) { validator: (value) {
@ -64,5 +75,4 @@ class AuthBoolField extends AuthField {
leftWidget: leftWidget, leftWidget: leftWidget,
rightWidget: rightWidget, rightWidget: rightWidget,
); );
}
} }

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_registration/flutter_registration.dart'; import "package:flutter_registration/flutter_registration.dart";
/// A field for capturing dropdown selections in a Flutter form. /// A field for capturing dropdown selections in a Flutter form.
/// ///
@ -10,11 +10,11 @@ class AuthDropdownField extends AuthField {
required super.name, required super.name,
required this.items, required this.items,
required this.onChanged, required this.onChanged,
required super.value,
this.dropdownDecoration, this.dropdownDecoration,
this.padding = const EdgeInsets.all(8.0), this.padding = const EdgeInsets.all(8.0),
this.textStyle, this.textStyle,
this.icon = const Icon(Icons.keyboard_arrow_down), this.icon = const Icon(Icons.keyboard_arrow_down),
required super.value,
}) { }) {
selectedValue = value ?? items.first; selectedValue = value ?? items.first;
} }
@ -41,23 +41,25 @@ class AuthDropdownField extends AuthField {
final Icon icon; final Icon icon;
@override @override
Widget build(BuildContext context, Function onValueChanged) { Widget build(BuildContext context, Function onValueChanged) => Padding(
return Padding(
padding: padding, padding: padding,
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
icon: icon, icon: icon,
style: textStyle, style: textStyle,
value: selectedValue, value: selectedValue,
decoration: dropdownDecoration, decoration: dropdownDecoration,
items: items.map((String value) { items: items
return DropdownMenuItem<String>( .map(
(String value) => DropdownMenuItem<String>(
value: value, value: value,
child: Text(value), child: Text(value),
); ),
}).toList(), )
.toList(),
onChanged: (newValue) { onChanged: (newValue) {
selectedValue = newValue; selectedValue = newValue;
onChanged(newValue); onChanged(newValue);
// ignore: avoid_dynamic_calls
onValueChanged(); onValueChanged();
}, },
validator: (value) { validator: (value) {
@ -73,5 +75,4 @@ class AuthDropdownField extends AuthField {
}, },
), ),
); );
}
} }

View file

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

View file

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // 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. /// An abstract class representing a field in a Flutter form.
/// ///
@ -14,11 +14,13 @@ abstract class AuthField<T> {
/// ///
/// [value] specifies the initial value 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). /// [onValueChanged] is a callback function triggered when the
/// value of the field changes (optional).
/// ///
/// [title] specifies the title widget of the field (optional). /// [title] specifies the title widget of the field (optional).
/// ///
/// [validators] defines a list of validation functions for the field (optional). /// [validators] defines a list of validation
/// functions for the field (optional).
AuthField({ AuthField({
required this.name, required this.name,
required this.value, required this.value,
@ -46,6 +48,7 @@ abstract class AuthField<T> {
/// ///
/// [context] The build context. /// [context] The build context.
/// ///
/// [onValueChanged] A function to be called when the value of the field changes. /// [onValueChanged] A function to be called when
/// the value of the field changes.
Widget build(BuildContext context, Function onValueChanged); Widget build(BuildContext context, Function onValueChanged);
} }

View file

@ -2,9 +2,9 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_input_library/flutter_input_library.dart'; import "package:flutter_input_library/flutter_input_library.dart";
import 'package:flutter_registration/flutter_registration.dart'; import "package:flutter_registration/flutter_registration.dart";
/// A field for capturing password inputs in a Flutter form. /// A field for capturing password inputs in a Flutter form.
/// ///
@ -18,25 +18,31 @@ class AuthPassField extends AuthField {
/// ///
/// [title] specifies the title widget of the field (optional). /// [title] specifies the title widget of the field (optional).
/// ///
/// [validators] defines a list of validation functions for 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). /// [value] specifies the initial value of the
/// field (default is an empty string).
/// ///
/// [textStyle] defines the text style for the password input. /// [textStyle] defines the text style for the password input.
/// ///
/// [onChange] is a callback function triggered when the value of the field changes. /// [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). /// [iconSize] specifies the size of the icon displayed
/// with the password input (optional).
/// ///
/// [textFieldDecoration] defines the decoration for the password input field (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)). /// [padding] defines the padding around the password input
/// field (default is EdgeInsets.all(8.0)).
AuthPassField({ AuthPassField({
required super.name, required super.name,
TextEditingController? textEditingController, this.textEditingController,
super.title, super.title,
super.validators = const [], super.validators = const [],
super.value = '', super.value = "",
this.textStyle, this.textStyle,
this.onChange, this.onChange,
this.iconSize, this.iconSize,
@ -44,15 +50,26 @@ class AuthPassField extends AuthField {
this.padding = const EdgeInsets.all(8.0), this.padding = const EdgeInsets.all(8.0),
}); });
/// The text style for the password input.
final TextStyle? textStyle; final TextStyle? textStyle;
/// The size of the icon displayed with the password input.
final double? iconSize; final double? iconSize;
/// A callback function triggered when the value of the field changes.
final Function(String value)? onChange; final Function(String value)? onChange;
/// The decoration for the password input field.
final InputDecoration? textFieldDecoration; final InputDecoration? textFieldDecoration;
/// The padding around the password input field.
final EdgeInsets padding; final EdgeInsets padding;
/// The controller for the password input.
final TextEditingController? textEditingController;
@override @override
Widget build(BuildContext context, Function onValueChanged) { Widget build(BuildContext context, Function onValueChanged) => Padding(
return Padding(
padding: padding, padding: padding,
child: FlutterFormInputPassword( child: FlutterFormInputPassword(
style: textStyle, style: textStyle,
@ -61,6 +78,7 @@ class AuthPassField extends AuthField {
onChanged: (v) { onChanged: (v) {
value = v; value = v;
onChange?.call(value); onChange?.call(value);
// ignore: avoid_dynamic_calls
onValueChanged(); onValueChanged();
}, },
validator: (value) { validator: (value) {
@ -75,5 +93,4 @@ class AuthPassField extends AuthField {
}, },
), ),
); );
}
} }

View file

@ -2,12 +2,15 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // 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 { class AuthStep {
/// Constructs an [AuthStep] object.
AuthStep({ AuthStep({
required this.fields, required this.fields,
}); });
/// The fields in the step.
List<AuthField> fields; List<AuthField> fields;
} }

View file

@ -2,8 +2,8 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_registration/flutter_registration.dart'; import "package:flutter_registration/flutter_registration.dart";
/// A field for capturing text inputs in a Flutter form. /// A field for capturing text inputs in a Flutter form.
/// ///
@ -17,49 +17,68 @@ class AuthTextField extends AuthField {
/// ///
/// [title] specifies the title widget of the field (optional). /// [title] specifies the title widget of the field (optional).
/// ///
/// [validators] defines a list of validation functions for 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). /// [value] specifies the initial value of the
/// field (default is an empty string).
/// ///
/// [textStyle] defines the text style for the text input. /// [textStyle] defines the text style for the text input.
/// ///
/// [onChange] is a callback function triggered when the value of the field changes. /// [onChange] is a callback function triggered
/// when the value of the field changes.
/// ///
/// [textFieldDecoration] defines the decoration for the text input field (optional). /// [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)). /// [padding] defines the padding around the text
/// input field (default is EdgeInsets.all(8.0)).
AuthTextField({ AuthTextField({
required super.name, required super.name,
TextEditingController? textEditingController, TextEditingController? textEditingController,
super.title, super.title,
super.validators = const [], super.validators = const [],
super.value = '', super.value = "",
this.textStyle, this.textStyle,
this.onChange, this.onChange,
this.textFieldDecoration, this.textFieldDecoration,
this.padding = const EdgeInsets.all(8.0), this.padding = const EdgeInsets.all(8.0),
this.textInputType,
}) { }) {
textController = textController =
textEditingController ?? TextEditingController(text: value); textEditingController ?? TextEditingController(text: value);
} }
/// The controller for the text input.
late TextEditingController textController; late TextEditingController textController;
/// The text style for the text input.
final TextStyle? textStyle; final TextStyle? textStyle;
/// A callback function triggered when the value of the field changes.
final Function(String value)? onChange; final Function(String value)? onChange;
/// The decoration for the text input field.
final InputDecoration? textFieldDecoration; final InputDecoration? textFieldDecoration;
/// The padding around the text input field.
final EdgeInsets padding; final EdgeInsets padding;
/// The type of text input.
final TextInputType? textInputType;
@override @override
Widget build(BuildContext context, Function onValueChanged) { Widget build(BuildContext context, Function onValueChanged) => Padding(
return Padding(
padding: padding, padding: padding,
child: TextFormField( child: TextFormField(
keyboardType: textInputType,
style: textStyle, style: textStyle,
decoration: textFieldDecoration, decoration: textFieldDecoration,
controller: textController, controller: textController,
onChanged: (v) { onChanged: (v) {
value = v; value = v;
onChange?.call(value); onChange?.call(value);
// ignore: avoid_dynamic_calls
onValueChanged(); onValueChanged();
}, },
validator: (value) { validator: (value) {
@ -74,5 +93,4 @@ class AuthTextField extends AuthField {
}, },
), ),
); );
}
} }

View file

@ -1,8 +1,8 @@
import 'dart:collection'; import "dart:collection";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_registration/flutter_registration.dart'; import "package:flutter_registration/flutter_registration.dart";
import 'package:flutter_registration/src/auth_screen.dart'; import "package:flutter_registration/src/auth_screen.dart";
/// A screen for user registration. /// A screen for user registration.
class RegistrationScreen extends StatefulWidget { class RegistrationScreen extends StatefulWidget {
@ -11,8 +11,8 @@ class RegistrationScreen extends StatefulWidget {
/// [registrationOptions] specifies the registration options. /// [registrationOptions] specifies the registration options.
const RegistrationScreen({ const RegistrationScreen({
required this.registrationOptions, required this.registrationOptions,
Key? key, super.key,
}) : super(key: key); });
/// The registration options. /// The registration options.
final RegistrationOptions registrationOptions; final RegistrationOptions registrationOptions;
@ -23,17 +23,13 @@ class RegistrationScreen extends StatefulWidget {
/// The state for [RegistrationScreen]. /// The state for [RegistrationScreen].
class RegistrationScreenState extends State<RegistrationScreen> { class RegistrationScreenState extends State<RegistrationScreen> {
bool _isLoading = false;
/// Registers the user. /// Registers the user.
Future<void> _register({ Future<void> _register({
required HashMap<String, dynamic> values, required HashMap<String, dynamic> values,
required void Function(int? pageToReturn) onError, required void Function(int? pageToReturn) onError,
}) async { }) async {
try { try {
setState(() { await widget.registrationOptions.beforeRegistration?.call();
_isLoading = true;
});
var registered = await widget.registrationOptions.registrationRepository! var registered = await widget.registrationOptions.registrationRepository!
.register(values); .register(values);
@ -45,12 +41,8 @@ class RegistrationScreenState extends State<RegistrationScreen> {
onError(pageToReturn); onError(pageToReturn);
} }
} catch (e) { } on Exception catch (_) {
onError(0); onError(null);
} finally {
setState(() {
_isLoading = false;
});
} }
} }
@ -75,7 +67,6 @@ class RegistrationScreenState extends State<RegistrationScreen> {
customBackgroundColor: widget.registrationOptions.backgroundColor, customBackgroundColor: widget.registrationOptions.backgroundColor,
titleWidget: widget.registrationOptions.titleWidget, titleWidget: widget.registrationOptions.titleWidget,
loginButton: widget.registrationOptions.loginButton, loginButton: widget.registrationOptions.loginButton,
isLoading: _isLoading,
titleFlex: widget.registrationOptions.titleFlex, titleFlex: widget.registrationOptions.titleFlex,
formFlex: widget.registrationOptions.formFlex, formFlex: widget.registrationOptions.formFlex,
beforeTitleFlex: widget.registrationOptions.beforeTitleFlex, beforeTitleFlex: widget.registrationOptions.beforeTitleFlex,

View file

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

View file

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

View file

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