diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 9b187dd5..e80cc9ca 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -16,7 +16,7 @@ jobs: steps: - name: 📥 Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: ⚡ Set Up Flutter uses: subosito/flutter-action@v2 @@ -50,7 +50,7 @@ jobs: steps: - name: 📥 Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: ⚡ Set Up Flutter uses: subosito/flutter-action@v2 @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 📥 Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: ⚡ Set Up Flutter uses: subosito/flutter-action@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index fadf955d..f47c5a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 6.0.0 +Breaking Changes: +- Replaced intl_phone_number_input with form_builder_phone_field, removing need for platform specific code. +This change makes the plugin WASM compatible & fixes build errors on Android. + +Other changes: +- Updated dependencies, fixed deprecated member usages + ## 5.1.0 Features: - Added background image (thanks @alenas !) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 50250b4a..7c298183 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -23,7 +23,7 @@ if (flutterVersionName == null) { } android { - compileSdk 35 + compileSdk 36 compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -41,7 +41,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" minSdkVersion flutter.minSdkVersion - targetSdkVersion 35 + targetSdkVersion 36 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' @@ -67,6 +67,6 @@ flutter { dependencies { testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.2.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index c0aacdb2..e03ab7cb 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Aug 29 15:49:40 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 641f9339..ca751158 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" // apply true - id "com.android.application" version '8.10.1' apply false - id "org.jetbrains.kotlin.android" version "1.9.10" apply false + id "com.android.application" version '8.13.0' apply false + id "org.jetbrains.kotlin.android" version "2.2.20" apply false } include ":app" \ No newline at end of file diff --git a/example/lib/login_screen.dart b/example/lib/login_screen.dart index aef0f3de..32625c85 100644 --- a/example/lib/login_screen.dart +++ b/example/lib/login_screen.dart @@ -125,6 +125,11 @@ class LoginScreen extends StatelessWidget { return null; }, ), + const UserFormField( + keyName: 'int_phone_number', + displayName: 'Int Phone Number', + userType: LoginUserType.intlPhone, + ), ], // scrollable: true, // hideProvidersTitle: false, @@ -226,13 +231,13 @@ class LoginScreen extends StatelessWidget { // // shape: ContinuousRectangleBorder(borderRadius: BorderRadius.circular(55.0)), // ), // ), - userValidator: (value) { + userValidator: (value, _) { if (!value!.contains('@') || !value.endsWith('.com')) { return "Email must contain '@' and end with '.com'"; } return null; }, - passwordValidator: (value) { + passwordValidator: (value, _) { if (value!.isEmpty) { return 'Password is empty'; } diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index f6f23bfe..00000000 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47b..00000000 --- a/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake deleted file mode 100644 index f16b4c34..00000000 --- a/example/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,24 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - url_launcher_linux -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/example/pubspec.lock b/example/pubspec.lock index 861ec668..4818bd59 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + country_pickers: + dependency: transitive + description: + name: country_pickers + sha256: b10f6618fa64fbba02ffc4ad1b84dc0ca071cc206e5376de1698bddd980b355a + url: "https://pub.dev" + source: hosted + version: "3.0.1" cupertino_icons: dependency: "direct main" description: @@ -65,14 +73,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - equatable: - dependency: transitive - description: - name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" - url: "https://pub.dev" - source: hosted - version: "2.0.7" fake_async: dependency: transitive description: @@ -86,6 +86,19 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_form_builder: + dependency: transitive + description: + name: flutter_form_builder + sha256: ec74389c4af2361a5e9fe9a36fcfe722698be3f681d713cb3ebe099ae15ed863 + url: "https://pub.dev" + source: hosted + version: "10.2.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_login: dependency: "direct main" description: @@ -111,78 +124,46 @@ packages: url: "https://pub.dev" source: hosted version: "10.8.0" - intl: - dependency: "direct main" - description: - name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.dev" - source: hosted - version: "0.19.0" - intl_phone_number_input: + form_builder_phone_field: dependency: transitive description: - name: intl_phone_number_input - sha256: "1c4328713a9503ab26a1fdbb6b00b4cada68c18aac922b35bedbc72eff1297c3" + name: form_builder_phone_field + sha256: c89ea400428bb22707a8d76e433070bef4a74de15a085060e1d75f9e3b660cab url: "https://pub.dev" source: hosted - version: "0.7.4" - js: - dependency: transitive + version: "3.0.0" + intl: + dependency: "direct main" description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - libphonenumber_platform_interface: - dependency: transitive - description: - name: libphonenumber_platform_interface - sha256: f801f6c65523f56504b83f0890e6dad584ab3a7507dca65fec0eed640afea40f - url: "https://pub.dev" - source: hosted - version: "0.4.2" - libphonenumber_plugin: - dependency: transitive - description: - name: libphonenumber_plugin - sha256: c615021d9816fbda2b2587881019ed595ecdf54d999652d7e4cce0e1f026368c + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "0.3.3" - libphonenumber_web: - dependency: transitive - description: - name: libphonenumber_web - sha256: "8186f420dbe97c3132283e52819daff1e55d60d6db46f7ea5ac42f42a28cc2ef" - url: "https://pub.dev" - source: hosted - version: "0.3.2" + version: "3.0.2" matcher: dependency: transitive description: @@ -312,10 +293,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" url_launcher: dependency: transitive description: @@ -384,10 +365,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" very_good_analysis: dependency: "direct dev" description: @@ -413,5 +394,5 @@ packages: source: hosted version: "1.1.1" sdks: - dart: ">=3.7.0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f0ba5ac2..054e8da6 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: flutter_login: path: ../ font_awesome_flutter: ^10.0.0 - intl: ^0.19.0 + intl: ^0.20.0 dev_dependencies: flutter_test: diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 4f788487..00000000 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); -} diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85..00000000 --- a/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake deleted file mode 100644 index 88b22e5c..00000000 --- a/example/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,24 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - url_launcher_windows -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/lib/flutter_login.dart b/lib/flutter_login.dart index 5af46cd7..5918f987 100644 --- a/lib/flutter_login.dart +++ b/lib/flutter_login.dart @@ -243,6 +243,7 @@ class __HeaderState extends State<_Header> { widget.title!, key: kTitleKey, style: theme.textTheme.displaySmall, + textAlign: TextAlign.center, ); } else { title = null; @@ -409,14 +410,14 @@ class FlutterLogin extends StatefulWidget { /// Email validating logic, Returns an error string to display if the input is /// invalid, or null otherwise - final FormFieldValidator? userValidator; + final AuthModeAwareValidator? userValidator; /// Should email be validated after losing focus true or after form /// submissions false. Default: false final bool? validateUserImmediately; /// Same as [userValidator] but for password - final FormFieldValidator? passwordValidator; + final AuthModeAwareValidator? passwordValidator; /// Called after the submit animation's completed. Put your route transition /// logic here. Recommend to use with [logoTag] and [titleTag] @@ -540,7 +541,7 @@ class FlutterLogin extends StatefulWidget { /// Default email validator used when none is supplied. /// /// Returns `'Invalid email!'` if the value is null, empty, or doesn't match a basic email regex. - static String? defaultEmailValidator(String? value) { + static String? defaultEmailValidator(String? value, AuthMode authMode) { if (value == null || value.isEmpty || !email.hasMatch(value)) { return 'Invalid email!'; } @@ -550,7 +551,7 @@ class FlutterLogin extends StatefulWidget { /// Default password validator used when none is supplied. /// /// Returns `'Password is too short!'` if the value is null, empty, or shorter than 3 characters. - static String? defaultPasswordValidator(String? value) { + static String? defaultPasswordValidator(String? value, AuthMode authMode) { if (value == null || value.isEmpty || value.length <= 2) { return 'Password is too short!'; } diff --git a/lib/src/providers/auth.dart b/lib/src/providers/auth.dart index 1458a86e..0eb41ff2 100644 --- a/lib/src/providers/auth.dart +++ b/lib/src/providers/auth.dart @@ -61,6 +61,13 @@ typedef ConfirmSignupRequiredCallback = Future Function(LoginData); /// The result is an error message; callback succeeds if the message is null. typedef ConfirmRecoverCallback = Future? Function(String, LoginData); +/// Validator that also provides auth mode to the function, so that client code +/// can execute different validation for login/signup +typedef AuthModeAwareValidator = String? Function( + T? value, + AuthMode authMode, + ); + /// Provides and manages authentication state and callbacks. class Auth with ChangeNotifier { /// Creates an instance of [Auth] to manage the login/signup state and related callbacks. @@ -118,6 +125,7 @@ class Auth with ChangeNotifier { /// The type of authentication being used (password or provider). AuthType get authType => _authType; + set authType(AuthType authType) { _authType = authType; notifyListeners(); @@ -127,6 +135,7 @@ class Auth with ChangeNotifier { /// Current authentication mode (login or signup). AuthMode get mode => _mode; + set mode(AuthMode value) { _mode = value; notifyListeners(); @@ -160,6 +169,7 @@ class Auth with ChangeNotifier { /// Email or username entered by the user. String get email => _email; + set email(String email) { _email = email; notifyListeners(); @@ -169,6 +179,7 @@ class Auth with ChangeNotifier { /// Password entered by the user. String get password => _password; + set password(String password) { _password = password; notifyListeners(); @@ -178,6 +189,7 @@ class Auth with ChangeNotifier { /// Confirmation password entered by the user during sign up. String get confirmPassword => _confirmPassword; + set confirmPassword(String confirmPassword) { _confirmPassword = confirmPassword; notifyListeners(); diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 18e6326b..abc98cc2 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -20,4 +20,4 @@ const kMinLogoHeight = 50.0; /// The maximum height at which the logo will be displayed. /// /// Used to limit scaling of the logo in larger layouts. -const kMaxLogoHeight = 125.0; +const kMaxLogoHeight = 250.0; diff --git a/lib/src/widgets/animated_text.dart b/lib/src/widgets/animated_text.dart index 548ce6f0..8cd48cb0 100644 --- a/lib/src/widgets/animated_text.dart +++ b/lib/src/widgets/animated_text.dart @@ -120,40 +120,44 @@ class _AnimatedTextState extends State Matrix4 _getFrontSideUp(double value) { return _matrix - ..translate( - 0.0, + ..translateByDouble( + 0, -radius * sin(_animation.value), -radius * cos(_animation.value), + 1, ) ..rotateX(-_animation.value); // 0 -> -pi/2 } Matrix4 _getBackSideUp(double value) { return _matrix - ..translate( - 0.0, + ..translateByDouble( + 0, radius * cos(_animation.value), -radius * sin(_animation.value), + 1, ) ..rotateX((pi / 2) - _animation.value); // pi/2 -> 0 } Matrix4 _getFrontSideDown(double value) { return _matrix - ..translate( - 0.0, + ..translateByDouble( + 0, radius * sin(_animation.value), -radius * cos(_animation.value), + 1, ) ..rotateX(_animation.value); // 0 -> pi/2 } Matrix4 _getBackSideDown(double value) { return _matrix - ..translate( - 0.0, + ..translateByDouble( + 0, -radius * cos(_animation.value), -radius * sin(_animation.value), + 1, ) ..rotateX(_animation.value - pi / 2); // -pi/2 -> 0 } diff --git a/lib/src/widgets/animated_text_form_field.dart b/lib/src/widgets/animated_text_form_field.dart index 732fc807..09a74eac 100644 --- a/lib/src/widgets/animated_text_form_field.dart +++ b/lib/src/widgets/animated_text_form_field.dart @@ -2,11 +2,12 @@ import 'dart:math'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_login/flutter_login.dart'; import 'package:flutter_login/src/widgets/term_of_service_checkbox.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:intl_phone_number_input/intl_phone_number_input.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart' as pnp; +//import 'package:form_builder_phone_field/form_builder_phone_field.dart'; +//import 'package:phone_numbers_parser/phone_numbers_parser.dart' as pnp; import 'package:url_launcher/url_launcher.dart'; /// Represents the direction of inertial animation applied to a text field. @@ -171,7 +172,7 @@ class _AnimatedTextFormFieldState extends State { late Animation iconRotationAnimation; late Animation iconTranslateAnimation; - PhoneNumber? _phoneNumberInitialValue; + //PhoneNumber? _phoneNumberInitialValue; final TextEditingController _phoneNumberController = TextEditingController(); @override @@ -240,20 +241,20 @@ class _AnimatedTextFormFieldState extends State { ); } + /* if (widget.userType == LoginUserType.intlPhone) { - _phoneNumberInitialValue = PhoneNumber( - isoCode: widget.initialIsoCode ?? 'US', - dialCode: '+1', - ); + _phoneNumberController.text = pnp.PhoneNumber( + isoCode: pnp.IsoCode.fromJson(widget.initialIsoCode ?? 'US'), + nsn: '', + ).nsn; if (widget.controller?.value.text != null) { try { final parsed = pnp.PhoneNumber.parse(widget.controller!.value.text); if (parsed.isValid()) { - _phoneNumberInitialValue = PhoneNumber( - phoneNumber: parsed.nsn, - isoCode: parsed.isoCode.name, - dialCode: parsed.countryCode, - ); + _phoneNumberController.text = pnp.PhoneNumber( + nsn: parsed.nsn, + isoCode: pnp.IsoCode.fromJson(parsed.isoCode.name), + ).nsn; } } on pnp.PhoneNumberException { // ignore @@ -262,6 +263,8 @@ class _AnimatedTextFormFieldState extends State { } } } + + */ } void _updateSizeAnimation() { @@ -317,7 +320,7 @@ class _AnimatedTextFormFieldState extends State { builder: (context, child) => Transform( alignment: Alignment.center, transform: Matrix4.identity() - ..translate(iconTranslateAnimation.value) + ..translateByDouble(iconTranslateAnimation.value, 0, 0, 1) ..rotateZ(iconRotationAnimation.value), child: child, ), @@ -329,78 +332,51 @@ class _AnimatedTextFormFieldState extends State { return InputDecoration( labelText: widget.labelText, prefixIcon: _buildInertiaAnimation(widget.prefixIcon), - suffixIcon: _buildInertiaAnimation( - widget.loadingController != null - ? FadeTransition( - opacity: suffixIconOpacityAnimation, - child: widget.suffixIcon, - ) - : widget.suffixIcon, - ), + suffixIcon: widget.userType == LoginUserType.intlPhone + ? null + : _buildInertiaAnimation( + widget.loadingController != null + ? FadeTransition( + opacity: suffixIconOpacityAnimation, + child: widget.suffixIcon, + ) + : widget.suffixIcon, + ), ); } + final _formKey = GlobalKey(); + @override Widget build(BuildContext context) { final theme = Theme.of(context); Widget inputField; - if (widget.userType == LoginUserType.intlPhone) { - inputField = Padding( - padding: const EdgeInsets.only(left: 8), - child: InternationalPhoneNumberInput( + /*if (widget.userType == LoginUserType.intlPhone) { + _phoneNumberController.addListener(() { + final phoneNumber = (_formKey.currentState?.fields['phone_number_intl'] + as FormBuilderPhoneFieldState?) + ?.fullNumber; + if (phoneNumber == null) return; + widget.controller?.text = phoneNumber; + }); + + inputField = FormBuilder( + key: _formKey, + child: FormBuilderPhoneField( + name: 'phone_number_intl', + iconSelector: const SizedBox.shrink(), cursorColor: theme.primaryColor, focusNode: widget.focusNode, - inputDecoration: _getInputDecoration(theme), - searchBoxDecoration: const InputDecoration( - contentPadding: EdgeInsets.only(left: 20), - labelText: 'Search by country name or dial code', - ), + decoration: _getInputDecoration(theme), keyboardType: widget.keyboardType ?? TextInputType.phone, onFieldSubmitted: widget.onFieldSubmitted, - onSaved: (phoneNumber) { - if (phoneNumber.phoneNumber == phoneNumber.dialCode) { - widget.controller?.text = ''; - } else { - widget.controller?.text = phoneNumber.phoneNumber ?? ''; - } - _phoneNumberController.selection = TextSelection.collapsed( - offset: _phoneNumberController.text.length, - ); - widget.onSaved?.call(phoneNumber.phoneNumber); - }, validator: widget.validator, - autofillHints: widget.autofillHints, - onInputChanged: (phoneNumber) { - if (phoneNumber.phoneNumber != null && - phoneNumber.dialCode != null && - phoneNumber.phoneNumber!.startsWith('+')) { - _phoneNumberController.text = - _phoneNumberController.text.replaceAll( - RegExp( - '^([\\+]${phoneNumber.dialCode!.replaceAll('+', '')}[\\s]?)', - ), - '', - ); - } - _phoneNumberController.selection = TextSelection.collapsed( - offset: _phoneNumberController.text.length, - ); - }, - textFieldController: _phoneNumberController, - isEnabled: widget.enabled, - selectorConfig: SelectorConfig( - selectorType: PhoneInputSelectorType.DIALOG, - trailingSpace: false, - countryComparator: (c1, c2) => - int.parse(c1.dialCode!.substring(1)).compareTo( - int.parse(c2.dialCode!.substring(1)), - ), - ), - spaceBetweenSelectorAndTextField: 0, - initialValue: _phoneNumberInitialValue, + controller: _phoneNumberController, + enabled: widget.enabled, ), ); - } else if (widget.userType == LoginUserType.checkbox) { + } else*/ + if (widget.userType == LoginUserType.checkbox) { inputField = CheckboxFormField( initialValue: widget.controller?.text == 'true', validator: (value) => diff --git a/lib/src/widgets/cards/auth_card_builder.dart b/lib/src/widgets/cards/auth_card_builder.dart index 9c8485f3..d0091826 100644 --- a/lib/src/widgets/cards/auth_card_builder.dart +++ b/lib/src/widgets/cards/auth_card_builder.dart @@ -76,13 +76,13 @@ class AuthCard extends StatefulWidget { final AnimationController loadingController; /// Validator for the user input field (email/username/etc). - final FormFieldValidator? userValidator; + final AuthModeAwareValidator? userValidator; /// Whether to validate the user field immediately on blur. final bool? validateUserImmediately; /// Validator for the password input field. - final FormFieldValidator? passwordValidator; + final AuthModeAwareValidator? passwordValidator; /// Called when the form is submitted (e.g., login/signup). final VoidCallback? onSubmit; @@ -524,7 +524,8 @@ class AuthCardState extends State with TickerProviderStateMixin { case _confirmRecover: return _ConfirmRecoverCard( key: _confirmRecoverCardKey, - passwordValidator: widget.passwordValidator!, + passwordValidator: (s) => + widget.passwordValidator!.call(s, AuthMode.signup), onBack: () => _changeCard(_loginPageIndex), onSubmitCompleted: () => _changeCard(_loginPageIndex), initialIsoCode: widget.initialIsoCode, @@ -606,8 +607,10 @@ class AuthCardState extends State with TickerProviderStateMixin { alignment: Alignment.center, transform: Matrix4.identity() ..rotateZ(_cardRotationAnimation.value) - ..scale(_cardSizeAnimation.value, _cardSizeAnimation.value) - ..scale(_cardSize2AnimationX.value, _cardSize2AnimationY.value), + ..scaleByDouble(_cardSizeAnimation.value, _cardSizeAnimation.value, + _cardSizeAnimation.value, 1) + ..scaleByDouble(_cardSize2AnimationX.value, + _cardSize2AnimationY.value, _cardSize2AnimationX.value, 1), child: current, ); }, diff --git a/lib/src/widgets/cards/login_card.dart b/lib/src/widgets/cards/login_card.dart index b59ee0b8..96d16bbf 100644 --- a/lib/src/widgets/cards/login_card.dart +++ b/lib/src/widgets/cards/login_card.dart @@ -33,9 +33,9 @@ class _LoginCard extends StatefulWidget { }); final AnimationController loadingController; - final FormFieldValidator? userValidator; + final AuthModeAwareValidator? userValidator; final bool? validateUserImmediately; - final FormFieldValidator? passwordValidator; + final AuthModeAwareValidator? passwordValidator; final VoidCallback onSwitchRecoveryPassword; final VoidCallback onSwitchSignUpAdditionalData; final VoidCallback onSwitchConfirmSignup; @@ -401,7 +401,7 @@ class _LoginCardState extends State<_LoginCard> with TickerProviderStateMixin { onFieldSubmitted: (value) { FocusScope.of(context).requestFocus(_passwordFocusNode); }, - validator: widget.userValidator, + validator: (s) => widget.userValidator?.call(s, auth.mode), onSaved: (value) => auth.email = value!, enabled: !_isSubmitting, initialIsoCode: widget.initialIsoCode, @@ -431,7 +431,8 @@ class _LoginCardState extends State<_LoginCard> with TickerProviderStateMixin { FocusScope.of(context).requestFocus(_confirmPasswordFocusNode); } }, - validator: widget.passwordValidator, + validator: (String? value) => + widget.passwordValidator?.call(value, auth.mode), onSaved: (value) => auth.password = value!, enabled: !_isSubmitting, initialIsoCode: widget.initialIsoCode, diff --git a/lib/src/widgets/cards/recover_card.dart b/lib/src/widgets/cards/recover_card.dart index a8298d82..a1e8f251 100644 --- a/lib/src/widgets/cards/recover_card.dart +++ b/lib/src/widgets/cards/recover_card.dart @@ -13,7 +13,7 @@ class _RecoverCard extends StatefulWidget { this.loginTheme, }); - final FormFieldValidator? userValidator; + final AuthModeAwareValidator? userValidator; final VoidCallback onBack; final LoginUserType userType; final LoginTheme? loginTheme; @@ -109,7 +109,7 @@ class _RecoverCardState extends State<_RecoverCard> autofillHints: [getAutofillHints(widget.userType)], textInputAction: TextInputAction.done, onFieldSubmitted: (value) => _submit(), - validator: widget.userValidator, + validator: (s) => widget.userValidator?.call(s, AuthMode.login), onSaved: (value) => auth.email = value!, initialIsoCode: widget.initialIsoCode, autofocus: widget.autofocusName, diff --git a/lib/src/widgets/custom_page_transformer.dart b/lib/src/widgets/custom_page_transformer.dart index cf8941d6..757d9ec7 100644 --- a/lib/src/widgets/custom_page_transformer.dart +++ b/lib/src/widgets/custom_page_transformer.dart @@ -20,13 +20,16 @@ class CustomPageTransformer extends PageTransformer { final pageDt = 1 - position.abs(); // Apply scale and Y-axis rotation based on the page's position. + final scale = lerp(0.6, 1, pageDt); if (position > 0) { transform - ..scale(lerp(0.6, 1, pageDt)) // Scale up as it approaches center + ..scaleByDouble( + scale, scale, scale, 1) // Scale up as it approaches center ..rotateY(position * -1.5); // Rotate left for right-side pages } else { transform - ..scale(lerp(0.6, 1, pageDt)) // Scale up as it approaches center + ..scaleByDouble( + scale, scale, scale, 1) // Scale up as it approaches center ..rotateY(position * 1.5); // Rotate right for left-side pages } diff --git a/pubspec.yaml b/pubspec.yaml index 3ca9e0bf..a1b42ddc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_login description: A login widget with login/signup functionalities to help speed up development -version: 5.1.0 +version: 6.0.0 repository: https://github.com/NearHuscarl/flutter_login screenshots: - description: 'This screenshot shows the default themed login screen..' @@ -15,8 +15,11 @@ dependencies: another_transformer_page_view: ^2.0.0 flutter: sdk: flutter + flutter_form_builder: ^10.1.0 + flutter_localizations: + sdk: flutter font_awesome_flutter: ^10.0.0 - intl_phone_number_input: ^0.7.4 + form_builder_phone_field: ^3.0.0 phone_numbers_parser: ^9.0.0 provider: ^6.0.1 quiver: ^3.0.1 @@ -27,7 +30,8 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.16 - very_good_analysis: ^8.0.0 + very_good_analysis: ^9.0.0 + lint: ^2.8.0 flutter: uses-material-design: true diff --git a/test/flutter_login_test.dart b/test/flutter_login_test.dart index 827f434d..317503e8 100644 --- a/test/flutter_login_test.dart +++ b/test/flutter_login_test.dart @@ -183,8 +183,8 @@ void main() { onSignup: (data) => null, onLogin: (data) => null, onRecoverPassword: (data) => null, - userValidator: (value) => - value!.endsWith('.com') ? null : 'Invalid!', + userValidator: (value, authMode) => + value!.endsWith('.com') ? null : 'Invalid!', ), ); await tester.pumpWidget(loginBuilder()); @@ -216,8 +216,8 @@ void main() { onSignup: (data) => null, onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => - value!.length == 5 ? null : 'Invalid!', + passwordValidator: (value, authMode) => + value!.length == 5 ? null : 'Invalid!', ), ); await tester.pumpWidget(loginBuilder()); @@ -532,8 +532,8 @@ void main() { await tester.pumpAndSettle(); verifyInOrder([ - mockCallback.userValidator('invalid-name'), - mockCallback.passwordValidator(user.password), + mockCallback.userValidator('invalid-name', AuthMode.login), + mockCallback.passwordValidator(user.password, AuthMode.login), ]); verifyNever(mockCallback.onLogin(any)); verifyNever(mockCallback.onSubmitAnimationCompleted()); @@ -550,8 +550,8 @@ void main() { await tester.pumpAndSettle(); verifyInOrder([ - mockCallback.userValidator(invalidUser.name), - mockCallback.passwordValidator(invalidUser.password), + mockCallback.userValidator(invalidUser.name, AuthMode.login), + mockCallback.passwordValidator(invalidUser.password, AuthMode.login), mockCallback.onLogin(any), ]); verifyNever(mockCallback.onSubmitAnimationCompleted()); @@ -568,8 +568,8 @@ void main() { await tester.pumpAndSettle(); verifyInOrder([ - mockCallback.userValidator(user.name), - mockCallback.passwordValidator(user.password), + mockCallback.userValidator(user.name, AuthMode.login), + mockCallback.passwordValidator(user.password, AuthMode.login), mockCallback.onLogin(any), mockCallback.onSubmitAnimationCompleted(), ]); @@ -612,8 +612,18 @@ void main() { clickSubmitButton(); await tester.pumpAndSettle(); - verifyNever(mockCallback.userValidator(invalidUser.name)); - verifyNever(mockCallback.passwordValidator(invalidUser.password)); + verifyNever( + mockCallback.userValidator( + invalidUser.name, + AuthMode.signup, + ), + ); + verifyNever( + mockCallback.passwordValidator( + invalidUser.password, + AuthMode.signup, + ), + ); verifyNever(mockCallback.onSignup(any)); verifyNever(mockCallback.onSubmitAnimationCompleted()); @@ -631,8 +641,8 @@ void main() { await tester.pumpAndSettle(); verifyInOrder([ - mockCallback.userValidator('invalid-name'), - mockCallback.passwordValidator(user.password), + mockCallback.userValidator('invalid-name', AuthMode.signup), + mockCallback.passwordValidator(user.password, AuthMode.signup), ]); verifyNever(mockCallback.onSignup(any)); verifyNever(mockCallback.onSubmitAnimationCompleted()); @@ -654,8 +664,8 @@ void main() { await tester.pumpAndSettle(); verifyInOrder([ - mockCallback.userValidator(invalidUser.name), - mockCallback.passwordValidator(invalidUser.password), + mockCallback.userValidator(invalidUser.name, AuthMode.signup), + mockCallback.passwordValidator(invalidUser.password, AuthMode.signup), mockCallback.onSignup(any), ]); verifyNever(mockCallback.onSubmitAnimationCompleted()); @@ -674,8 +684,8 @@ void main() { await tester.pumpAndSettle(); verifyInOrder([ - mockCallback.userValidator(user.name), - mockCallback.passwordValidator(user.password), + mockCallback.userValidator(user.name, AuthMode.signup), + mockCallback.passwordValidator(user.password, AuthMode.signup), mockCallback.onSignup(any), mockCallback.onSubmitAnimationCompleted(), ]); @@ -697,11 +707,13 @@ void main() { additionalSignupFields: [ UserFormField( keyName: 'Name', - fieldValidator: mockCallback.userValidator, + fieldValidator: (s) => + mockCallback.userValidator(s, AuthMode.signup), ), UserFormField( keyName: 'Surname', - fieldValidator: mockCallback.userValidator, + fieldValidator: (s) => + mockCallback.userValidator(s, AuthMode.signup), ), ], ), @@ -728,8 +740,18 @@ void main() { clickSubmitButton(); await tester.pumpAndSettle(); - verifyNever(mockCallback.userValidator(invalidUser.name)); - verifyNever(mockCallback.passwordValidator(invalidUser.password)); + verifyNever( + mockCallback.userValidator( + invalidUser.name, + AuthMode.signup, + ), + ); + verifyNever( + mockCallback.passwordValidator( + invalidUser.password, + AuthMode.signup, + ), + ); verifyNever(mockCallback.onSignup(any)); verifyNever(mockCallback.onSubmitAnimationCompleted()); @@ -747,8 +769,8 @@ void main() { await tester.pumpAndSettle(); verifyInOrder([ - mockCallback.userValidator('invalid-name'), - mockCallback.passwordValidator(user.password), + mockCallback.userValidator('invalid-name', AuthMode.signup), + mockCallback.passwordValidator(user.password, AuthMode.signup), ]); verifyNever(mockCallback.onSignup(any)); verifyNever(mockCallback.onSubmitAnimationCompleted()); @@ -770,8 +792,8 @@ void main() { await tester.pumpAndSettle(); verifyInOrder([ - mockCallback.userValidator(invalidUser.name), - mockCallback.passwordValidator(invalidUser.password), + mockCallback.userValidator(invalidUser.name, AuthMode.signup), + mockCallback.passwordValidator(invalidUser.password, AuthMode.signup), ]); verifyNever(mockCallback.onSignup(any)); verifyNever(mockCallback.onSubmitAnimationCompleted()); @@ -796,8 +818,8 @@ void main() { await tester.pumpAndSettle(); verifyInOrder([ - mockCallback.userValidator('foo'), - mockCallback.userValidator('bar'), + mockCallback.userValidator('foo', AuthMode.signup), + mockCallback.userValidator('bar', AuthMode.signup), mockCallback.onSignup(any), mockCallback.onSubmitAnimationCompleted(), ]); @@ -899,7 +921,7 @@ void main() { FlutterLogin( onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => + passwordValidator: (value, authMode) => value!.length == 5 ? null : 'Invalid!', hideForgotPasswordButton: true, messages: LoginMessages( @@ -921,7 +943,7 @@ void main() { onSignup: (data) => null, onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => + passwordValidator: (value, authMode) => value!.length == 5 ? null : 'Invalid!', loginProviders: [ LoginProvider( @@ -949,8 +971,8 @@ void main() { onSignup: (data) => null, onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => - value!.length == 5 ? null : 'Invalid!', + passwordValidator: (value, authMode) => + value!.length == 5 ? null : 'Invalid!', messages: LoginMessages( signupButton: 'REGISTER', forgotPasswordButton: 'Forgot huh?', @@ -968,7 +990,7 @@ void main() { onSignup: (data) => null, onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => + passwordValidator: (value, authMode) => value!.length == 5 ? null : 'Invalid!', hideProvidersTitle: true, loginProviders: [ @@ -1056,7 +1078,7 @@ void main() { onSignup: (data) => null, onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => null, + passwordValidator: (value, authMode) => null, ), ); await tester.pumpWidget(loginBuilder()); @@ -1092,7 +1114,7 @@ void main() { }, onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => null, + passwordValidator: (value, authMode) => null, additionalSignupFields: const [ UserFormField(keyName: 'Name'), UserFormField(keyName: 'Surname'), @@ -1146,7 +1168,7 @@ void main() { onSignup: (data) => null, onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => null, + passwordValidator: (value, authMode) => null, additionalSignupFields: const [ UserFormField(keyName: 'Name'), UserFormField(keyName: 'Surname'), @@ -1192,7 +1214,7 @@ void main() { onSignup: (data) => null, onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => null, + passwordValidator: (value, authMode) => null, additionalSignupFields: const [ UserFormField(keyName: 'Name'), UserFormField(keyName: 'Surname'), @@ -1238,7 +1260,7 @@ void main() { onSignup: (data) => null, onLogin: (data) => null, onRecoverPassword: (data) => null, - passwordValidator: (value) => null, + passwordValidator: (value, authMode) => null, footer: 'Copyright flutter_login', ), ); diff --git a/test/utils.dart b/test/utils.dart index c083ce8b..a5336760 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -12,8 +12,8 @@ class LoginCallback { Future? onLogin(LoginData? data) => null; Future? onSignup(SignupData? data) => null; Future? onRecoverPassword(String? data) => null; - String? userValidator(String? value) => null; - String? passwordValidator(String? value) => null; + String? userValidator(String? value, AuthMode authMode) => null; + String? passwordValidator(String? value, AuthMode authMode) => null; void onSubmitAnimationCompleted() {} } @@ -27,11 +27,14 @@ List loginStubCallback(MockCallback mockCallback) { const user = LoginData(name: 'near@gmail.com', password: '12345'); const invalidUser = LoginData(name: 'not.exists@gmail.com', password: ''); - when(mockCallback.userValidator(user.name)).thenReturn(null); - when(mockCallback.userValidator('invalid-name')).thenReturn('Invalid!'); + when(mockCallback.userValidator(user.name, AuthMode.login)).thenReturn(null); + when(mockCallback.userValidator('invalid-name', AuthMode.login)) + .thenReturn('Invalid!'); - when(mockCallback.passwordValidator(user.password)).thenReturn(null); - when(mockCallback.passwordValidator('invalid-name')).thenReturn('Invalid!'); + when(mockCallback.passwordValidator(user.password, AuthMode.login)) + .thenReturn(null); + when(mockCallback.passwordValidator('invalid-name', AuthMode.login)) + .thenReturn('Invalid!'); when(mockCallback.onLogin(user)).thenAnswer((_) => null); when(mockCallback.onLogin(invalidUser)) @@ -48,11 +51,14 @@ List signupStubCallback(MockCallback mockCallback) { const invalidUser = SignupData.fromSignupForm(name: 'not.exists@gmail.com', password: ''); - when(mockCallback.userValidator(user.name)).thenReturn(null); - when(mockCallback.userValidator('invalid-name')).thenReturn('Invalid!'); + when(mockCallback.userValidator(user.name, AuthMode.signup)).thenReturn(null); + when(mockCallback.userValidator('invalid-name', AuthMode.signup)) + .thenReturn('Invalid!'); - when(mockCallback.passwordValidator(user.password)).thenReturn(null); - when(mockCallback.passwordValidator('invalid-name')).thenReturn('Invalid!'); + when(mockCallback.passwordValidator(user.password, AuthMode.signup)) + .thenReturn(null); + when(mockCallback.passwordValidator('invalid-name', AuthMode.signup)) + .thenReturn('Invalid!'); when(mockCallback.onSignup(user)).thenAnswer((_) => null); when(mockCallback.onSignup(invalidUser))