From 6a09c2ec07c9ff5e09a909d4d586b561baa127d9 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Mon, 15 Sep 2025 09:22:37 +0300 Subject: [PATCH] validations & otp widget & focus & auto fill --- lib/core/api_consts.dart | 2 +- lib/core/dependencies.dart | 6 - lib/core/utils/utils.dart | 83 ++- lib/core/utils/validation_utils.dart | 78 +++ .../authentication_view_model.dart | 137 ++-- .../widgets/otp_verification_screen.dart | 633 +++++++++++++++--- lib/presentation/authentication/login.dart | 63 +- lib/presentation/authentication/register.dart | 59 +- .../authentication/saved_login_screen.dart | 16 +- lib/services/dialog_service.dart | 179 ++++- lib/services/error_handler_service.dart | 13 +- lib/widgets/appbar/app_bar_widget.dart | 6 +- .../bottomsheet/generic_bottom_sheet.dart | 20 + lib/widgets/common_bottom_sheet.dart | 36 +- .../dropdown/country_dropdown_widget.dart | 15 +- lib/widgets/input_widget.dart | 122 ++-- lib/widgets/otp_widget.dart | 377 ----------- 17 files changed, 1110 insertions(+), 735 deletions(-) delete mode 100644 lib/widgets/otp_widget.dart diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index bad2e4c..42f10f4 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -726,7 +726,7 @@ const DEACTIVATE_ACCOUNT = 'Services/Patients.svc/REST/PatientAppleActivation_In class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index f86ec97..6edd3cc 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -145,12 +145,6 @@ class AppDependencies { ), ); - getIt.registerLazySingleton( - () => HabibWalletViewModel( - habibWalletRepo: getIt(), - errorHandlerService: getIt(), - ), - ); getIt.registerLazySingleton( () => MedicalFileViewModel( diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index e228591..09f455b 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -251,45 +251,43 @@ class Utils { ); } - static bool isSAUDIIDValid(String id, type) { - if (type == 1) { - try { - id = id.toString(); - id = id.trim(); - var returnValue = int.parse(id); - var sum = 0; - if (returnValue > 0) { - var type = int.parse(id[0]); - - if (id.length != 10) { - return false; - } - if (type != 2 && type != 1) { - return false; - } + static bool isSAUDIIDValid( + String id, + ) { + try { + id = id.toString(); + id = id.trim(); + var returnValue = int.parse(id); + var sum = 0; + if (returnValue > 0) { + var type = int.parse(id[0]); + + if (id.length != 10) { + return false; + } + if (type != 2 && type != 1) { + return false; + } - for (var i = 0; i < 10; i++) { - if (i % 2 == 0) { - var a = id[i]; - var x = int.parse(a) * 2; - var b = x.toString(); - if (b.length == 1) { - b = "0$b"; - } - sum += int.parse(b[0]) + int.parse(b[1]); - } else { - sum += int.parse(id[i]); + for (var i = 0; i < 10; i++) { + if (i % 2 == 0) { + var a = id[i]; + var x = int.parse(a) * 2; + var b = x.toString(); + if (b.length == 1) { + b = "0$b"; } + sum += int.parse(b[0]) + int.parse(b[1]); + } else { + sum += int.parse(id[i]); } - return sum % 10 == 0; } - } catch (err) { - log("errr: ${err.toString()}"); + return sum % 10 == 0; } - return false; - } else { - return true; + } catch (err) { + log("errr: ${err.toString()}"); } + return false; } static Widget getNoDataWidget(BuildContext context, {String? errorText}) { @@ -396,14 +394,14 @@ class Utils { return ''; } - // Replace HTML line breaks with newlines +// Replace HTML line breaks with newlines var withLineBreaks = htmlString.replaceAll(RegExp(r'', multiLine: true), '\n').replaceAll(RegExp(r'<\/p>', multiLine: true), '\n').replaceAll(RegExp(r'', multiLine: true), '\n'); - // Remove all other HTML tags +// Remove all other HTML tags var withoutTags = withLineBreaks.replaceAll(RegExp(r'<[^>]*>'), ''); - // Decode HTML entities +// Decode HTML entities var decodedString = withoutTags .replaceAll(' ', ' ') .replaceAll('&', '&') @@ -416,7 +414,7 @@ class Utils { .replaceAll('”', '"') .replaceAll('“', '"'); - // Remove extra whitespace and normalize line breaks +// Remove extra whitespace and normalize line breaks var normalizedString = decodedString .replaceAll(RegExp(r'\n\s*\n'), '\n\n') // Replace multiple blank lines with double line break .replaceAll(RegExp(r' +'), ' ') // Replace multiple spaces with single space @@ -427,13 +425,13 @@ class Utils { Widget mDivider(Color color) { return Divider( - // width: double.infinity, +// width: double.infinity, height: 1, color: color, ); } - // New Ui Items +// New Ui Items static String formatDateToDisplay(String isoDateString) { try { @@ -441,7 +439,7 @@ class Utils { final day = dateTime.day.toString().padLeft(2, '0'); final year = dateTime.year.toString(); - // Map month number to short month name +// Map month number to short month name const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; final month = monthNames[dateTime.month - 1]; @@ -467,7 +465,7 @@ class Utils { static String formatHijriDateToDisplay(String hijriDateString) { try { - // Assuming hijriDateString is in the format yyyy-MM-dd +// Assuming hijriDateString is in the format yyyy-MM-dd final datePart = hijriDateString.split("T").first; final parts = datePart.split('-'); if (parts.length != 3) return ""; @@ -475,7 +473,7 @@ class Utils { final day = parts[2].padLeft(2, '0'); final year = parts[0]; - // Map month number to short month name (Hijri months) +// Map month number to short month name (Hijri months) const hijriMonthNames = ['Muharram', 'Safar', 'Rabi I', 'Rabi II', 'Jumada I', 'Jumada II', 'Rajab', 'Sha\'ban', 'Ramadan', 'Shawwal', 'Dhu al-Qi\'dah', 'Dhu al-Hijjah']; final monthIndex = int.tryParse(parts[1]) ?? 1; final month = hijriMonthNames[monthIndex - 1]; @@ -641,5 +639,4 @@ class Utils { await file.writeAsBytes(bytes); return file.path; } - } diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart index cb54af5..098059c 100644 --- a/lib/core/utils/validation_utils.dart +++ b/lib/core/utils/validation_utils.dart @@ -1,6 +1,8 @@ import 'dart:developer'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; class ValidationUtils { @@ -20,4 +22,80 @@ class ValidationUtils { } return true; } + + static bool isValidatedId({String? nationalId, required Function() onOkPress, CountryEnum? selectedCountry, bool? isTermsAccepted, String? dob}) { + bool isCorrectID = true; + if (nationalId == null || nationalId.isEmpty) { + _dialogService.showExceptionBottomSheet(message: "Please enter a national ID", onOkPressed: onOkPress); + isCorrectID = false; + } + + if (nationalId != null && nationalId.isNotEmpty && selectedCountry != null) { + if (selectedCountry == CountryEnum.saudiArabia) { + if (!validateIqama(nationalId)) { + _dialogService.showExceptionBottomSheet(message: "Please enter a valid national ID", onOkPressed: onOkPress); + return false; + } + } + + if (selectedCountry == CountryEnum.unitedArabEmirates) { + if (!validateUaeNationalId(nationalId)) { + _dialogService.showExceptionBottomSheet(message: "Please enter a valid national ID", onOkPressed: onOkPress); + return false; + } + } + + if (dob == null || dob.isEmpty) { + _dialogService.showExceptionBottomSheet(message: "Please enter a valid date of birth", onOkPressed: onOkPress); + return false; + } + + if (isTermsAccepted != null && !isTermsAccepted) { + _dialogService.showExceptionBottomSheet(message: "Please accept the terms and conditions", onOkPressed: onOkPress); + return false; + } + } + return isCorrectID; + } + + static bool isValidatePhone({String? phoneNumber, required Function() onOkPress}) { + if (phoneNumber == null || phoneNumber.isEmpty) { + _dialogService.showExceptionBottomSheet(message: "Please enter a valid phone number", onOkPressed: onOkPress); + return false; + } + return true; + } + + static bool isValidate({String? phoneNumber, required Function() onOkPress}) { + if (phoneNumber == null || phoneNumber.isEmpty) { + _dialogService.showExceptionBottomSheet(message: "Please enter a valid phone number", onOkPressed: onOkPress); + return false; + } + return true; + } + + static bool validateIqama(String iqamaNumber) { + String cleanedIqama = iqamaNumber.replaceAll(RegExp(r'[^0-9]'), ''); + if (cleanedIqama.length != 10) { + return false; + } + int firstDigit = int.parse(cleanedIqama[0]); + if (firstDigit != 2 && firstDigit != 1) { + return false; + } + int sum = 0; + for (int i = 0; i < 10; i++) { + int digit = int.parse(cleanedIqama[i]); + int weight = (i % 2 == 0) ? 2 : 1; // Alternate weights: 2, 1, 2, 1... + int product = digit * weight; + sum += (product > 9) ? product - 9 : product; // Sum digits if product > 9 + } + return sum % 10 == 0; + } + + static bool validateUaeNationalId(String id) { + // Must be exactly 15 digits + final regex = RegExp(r'^784\d{4}\d{7}\d{1}$'); + return regex.hasMatch(id); + } } diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index b2464f6..e17d576 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/material.dart'; import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; @@ -23,6 +24,7 @@ import 'package:hmg_patient_app_new/features/authentication/models/request_model import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_activation_code_resp_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/saved_login_screen.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; @@ -103,7 +105,7 @@ class AuthenticationViewModel extends ChangeNotifier { } } - void clearDefaultInputValues() { + Future clearDefaultInputValues() async { nationalIdController.clear(); phoneNumberController.clear(); dobController.clear(); @@ -265,10 +267,7 @@ class AuthenticationViewModel extends ChangeNotifier { if (phoneNumberController.text.isEmpty) { phoneNumberController.text = "504278212"; } - bool isValidated = ValidationUtils.isValidatePhoneAndId( - phoneNumber: phoneNumberController.text, - nationalId: nationalIdController.text, - ); + bool isValidated = ValidationUtils.isValidatePhoneAndId(phoneNumber: phoneNumberController.text, nationalId: nationalIdController.text); if (!isValidated) { return; @@ -289,7 +288,20 @@ class AuthenticationViewModel extends ChangeNotifier { final result = await _authenticationRepo.checkPatientAuthentication(checkPatientAuthenticationReq: checkPatientAuthenticationReq); result.fold( - (failure) async => await _errorHandlerService.handleError(failure: failure), + (failure) async => await _errorHandlerService.handleError( + failure: failure, + onUnHandledFailure: (failure) async { + LoaderBottomSheet.hideLoader(); + await _dialogService.showCommonBottomSheetWithoutH( + message: failure.message, + label: LocaleKeys.notice.tr(), + onOkPressed: () { + _navigationService.pushAndReplace(AppRoutes.register); + }, + onCancelPressed: () { + _navigationService.pop(); + }); + }), (apiResponse) async { if (apiResponse.messageStatus == 2) { LoaderBottomSheet.hideLoader(); @@ -335,7 +347,6 @@ class AuthenticationViewModel extends ChangeNotifier { if (checkIsUserComingForRegister(request: payload)) { _appState.setUserRegistrationPayload = RegistrationDataModelPayload.fromJson(payload); - print("====== Demo =========="); } final resultEither = await _authenticationRepo.sendActivationCodeRepo(sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er'); @@ -345,7 +356,11 @@ class AuthenticationViewModel extends ChangeNotifier { (apiResponse) async { if (apiResponse.messageStatus == 2) { LoaderBottomSheet.hideLoader(); - await _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage ?? "ErrorEmpty"); + await _dialogService.showCommonBottomSheetWithoutH( + message: apiResponse.errorMessage ?? "Something Went Wrong", + onOkPressed: () { + _navigationService.pop(); + }); } else { if (apiResponse.data != null && apiResponse.data['isSMSSent'] == true) { LoaderBottomSheet.hideLoader(); @@ -403,18 +418,25 @@ class AuthenticationViewModel extends ChangeNotifier { request["ForRegisteration"] = _appState.getUserRegistrationPayload.isRegister; request["isRegister"] = false; - // if (request.containsKey("OTP_SendType")) { - // request.remove("OTP_SendType"); - // print("====== Demo: Removed OTP_SendType for Register state"); - // } - - print("====== Req"); - final resultEither = await _authenticationRepo.checkActivationCodeRepo(newRequest: request, activationCode: activationCode.toString(), isRegister: true); LoaderBottomSheet.hideLoader(); - resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) { + resultEither.fold( + (failure) async => await _errorHandlerService.handleError( + failure: failure, + onUnHandledFailure: (failure) async { + LoaderBottomSheet.hideLoader(); + await _dialogService.showCommonBottomSheetWithoutH( + message: failure.message, + label: LocaleKeys.notice.tr(), + onOkPressed: () { + _navigationService.pushAndReplace(AppRoutes.register); + }, + onCancelPressed: () { + _navigationService.pop(); + }); + }), (apiResponse) { final activation = CheckActivationCode.fromJson(apiResponse.data as Map); if (_appState.getUserRegistrationPayload.isRegister == true) { //TODO: KSA Version Came Hre @@ -425,18 +447,20 @@ class AuthenticationViewModel extends ChangeNotifier { } }); } else { - final resultEither = await _authenticationRepo.checkActivationCodeRepo( - newRequest: CheckActivationCodeRegisterReq.fromJson(request), - activationCode: activationCode, - isRegister: false, - ); - - resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async { + final resultEither = await _authenticationRepo.checkActivationCodeRepo(newRequest: CheckActivationCodeRegisterReq.fromJson(request), activationCode: activationCode, isRegister: false); + + resultEither.fold( + (failure) async => await _errorHandlerService.handleError( + failure: failure, + onUnHandledFailure: (failure) async { + LoaderBottomSheet.hideLoader(); + await _dialogService.showCommonBottomSheetWithoutH(message: failure.message, label: LocaleKeys.notice.tr(), onOkPressed: () {}); + }, + ), (apiResponse) async { final activation = CheckActivationCode.fromJson(apiResponse.data as Map); if (activation.errorCode == '699') { // Todo: Hide Loader - // GifLoaderDialogUtils.hideDialog(context); LoaderBottomSheet.hideLoader(); onWrongActivationCode(activation.errorEndUserMessage); @@ -465,7 +489,7 @@ class AuthenticationViewModel extends ChangeNotifier { } LoaderBottomSheet.hideLoader(); insertPatientIMEIData(loginTypeEnum.toInt); - clearDefaultInputValues(); + await clearDefaultInputValues(); if (isUserAgreedBefore) { navigateToHomeScreen(); } else { @@ -595,7 +619,7 @@ class AuthenticationViewModel extends ChangeNotifier { Future onRegistrationStart({required OTPTypeEnum otpTypeEnum}) async { bool isOutSidePatient = selectedCountrySignup.countryCode == CountryEnum.unitedArabEmirates.countryCode ? true : false; - + LoaderBottomSheet.showLoader(); final request = await RequestUtils.getPatientAuthenticationRequest( phoneNumber: phoneNumberController.text, nationId: nationalIdController.text, @@ -639,7 +663,7 @@ class AuthenticationViewModel extends ChangeNotifier { print(apiResponse.data as Map); if (apiResponse.data["MessageStatus"] == 1) { //TODO: Here We Need to Show a Dialog Of Something in the case of Success. - clearDefaultInputValues(); // This will Clear All Default Values Of User. + await clearDefaultInputValues(); // This will Clear All Default Values Of User. _navigationService.pushAndReplace(AppRoutes.loginScreen); } } @@ -651,24 +675,22 @@ class AuthenticationViewModel extends ChangeNotifier { Future checkUserStatusForRegistration({required dynamic response, required dynamic request}) async { if (response is Map) { if (response["MessageStatus"] == 2) { + LoaderBottomSheet.hideLoader(); print(response["ErrorEndUserMessage"]); return; } if (response['hasFile'] == true) { //TODO: Show Here Ok And Cancel Dialog and On OKPress it will go for sendActivationCode - - _navigationService.context?.showBottomSheet( - child: ExceptionBottomSheet( - message: response["ErrorMessage"], - showCancel: true, - showOKButton: true, - onOkPressed: () { - _navigationService.popUntilNamed(AppRoutes.loginScreen); - }, - onCancelPressed: () { - _navigationService.pop(); - }, - )); + LoaderBottomSheet.hideLoader(); + _dialogService.showCommonBottomSheetWithoutH( + message: response["ErrorMessage"], + onOkPressed: () async { + await clearDefaultInputValues(); + _navigationService.pushAndReplace(AppRoutes.loginScreen); + }, + onCancelPressed: () { + _navigationService.pop(); + }); } else { request['forRegister'] = true; request['isRegister'] = true; @@ -727,41 +749,6 @@ class AuthenticationViewModel extends ChangeNotifier { ); } }); - - // this.authService.checkUserStatus(request).then((result) { - // // Keep loader active, continue to next step - // if (result is Map) { - // RegisterInfoResponse? resultSet; - // CheckUserStatusResponse res = CheckUserStatusResponse.fromJson(result as Map); - // nHICData = res; - // sharedPref.setObject(NHIC_DATA, res.toJson()); - // resultSet = RegisterInfoResponse.fromJson(res.toJson()); - // - // sendActivationCode(type, loginToken, resultSet, isSkipRegistration); - // } else { - // GifLoaderDialogUtils.hideDialog(context); - // context.showBottomSheet( - // child: ExceptionBottomSheet( - // message: result != null ? result : TranslationBase.of(context).somethingWentWrong, - // showCancel: false, - // onOkPressed: () { - // Navigator.of(context).pop(); - // }, - // ), - // ); - // } - // }).catchError((err) { - // GifLoaderDialogUtils.hideDialog(context); - // context.showBottomSheet( - // child: ExceptionBottomSheet( - // message: err.toString(), - // showCancel: false, - // onOkPressed: () { - // Navigator.of(context).pop(); - // }, - // ), - // ); - // }); } void setNHICData(dynamic data, dynamic request) { diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart index e009593..1ea9be5 100644 --- a/lib/features/authentication/widgets/otp_verification_screen.dart +++ b/lib/features/authentication/widgets/otp_verification_screen.dart @@ -1,12 +1,394 @@ import 'dart:async'; +import 'package:flutter/animation.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart'; +typedef OnDone = void Function(String text); + +class ProvidedPinBoxTextAnimation { + static AnimatedSwitcherTransitionBuilder scalingTransition = (child, animation) { + return ScaleTransition( + child: child, + scale: animation, + ); + }; + + static AnimatedSwitcherTransitionBuilder defaultNoTransition = (Widget child, Animation animation) { + return child; + }; +} + +class OTPWidget extends StatefulWidget { + final int maxLength; + final TextEditingController? controller; + + final Color defaultBorderColor; + final Color pinBoxColor; + final double pinBoxBorderWidth; + final double pinBoxRadius; + final bool hideDefaultKeyboard; + + final TextStyle? pinTextStyle; + final double pinBoxHeight; + final double pinBoxWidth; + final OnDone? onDone; + final bool hasError; + final Color errorBorderColor; + final Color textBorderColor; + final Function(String)? onTextChanged; + final bool autoFocus; + final FocusNode? focusNode; + final AnimatedSwitcherTransitionBuilder? pinTextAnimatedSwitcherTransition; + final Duration pinTextAnimatedSwitcherDuration; + final TextDirection textDirection; + final TextInputType keyboardType; + final EdgeInsets pinBoxOuterPadding; + + OTPWidget({ + Key? key, + this.maxLength = 4, + this.controller, + this.pinBoxWidth = 70.0, + this.pinBoxHeight = 100.0, + this.pinTextStyle, + this.onDone, + this.defaultBorderColor = Colors.black, + this.textBorderColor = Colors.black, + this.pinTextAnimatedSwitcherTransition, + this.pinTextAnimatedSwitcherDuration = const Duration(), + this.hasError = false, + this.errorBorderColor = Colors.red, + this.onTextChanged, + this.autoFocus = false, + this.focusNode, + this.textDirection = TextDirection.ltr, + this.keyboardType = TextInputType.number, + this.pinBoxOuterPadding = const EdgeInsets.symmetric(horizontal: 4.0), + this.pinBoxColor = Colors.white, + this.pinBoxBorderWidth = 2.0, + this.pinBoxRadius = 0, + this.hideDefaultKeyboard = false, + }) : super(key: key); + + @override + State createState() { + return OTPWidgetState(); + } +} + +class OTPWidgetState extends State with SingleTickerProviderStateMixin { + late AnimationController _highlightAnimationController; + late FocusNode focusNode; + String text = ""; + int currentIndex = 0; + List strList = []; + bool hasFocus = false; + + @override + void didUpdateWidget(OTPWidget oldWidget) { + super.didUpdateWidget(oldWidget); + focusNode = widget.focusNode ?? focusNode; + + if (oldWidget.maxLength < widget.maxLength) { + setState(() { + currentIndex = text.length; + }); + widget.controller?.text = text; + widget.controller?.selection = TextSelection.collapsed(offset: text.length); + } else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.length > 0 && text.length > widget.maxLength) { + setState(() { + text = text.substring(0, widget.maxLength); + currentIndex = text.length; + }); + widget.controller?.text = text; + widget.controller?.selection = TextSelection.collapsed(offset: text.length); + } + } + + _calculateStrList() { + if (strList.length > widget.maxLength) { + strList.length = widget.maxLength; + } + while (strList.length < widget.maxLength) { + strList.add(""); + } + } + + @override + void initState() { + super.initState(); + focusNode = widget.focusNode ?? FocusNode(); + _highlightAnimationController = AnimationController(vsync: this); + _initTextController(); + _calculateStrList(); + widget.controller!.addListener(_controllerListener); + focusNode.addListener(_focusListener); + } + + void _controllerListener() { + if (mounted == true) { + setState(() { + _initTextController(); + }); + var onTextChanged = widget.onTextChanged; + if (onTextChanged != null) { + onTextChanged(widget.controller?.text ?? ""); + } + } + } + + void _focusListener() { + if (mounted == true) { + setState(() { + hasFocus = focusNode.hasFocus; + }); + } + } + + void _initTextController() { + if (widget.controller == null) { + return; + } + strList.clear(); + var text = widget.controller?.text ?? ""; + if (text.isNotEmpty) { + if (text.length > widget.maxLength) { + throw Exception("TextEditingController length exceeded maxLength!"); + } + } + for (var i = 0; i < text.length; i++) { + strList.add(text[i]); + } + } + + double get _width { + var width = 0.0; + for (var i = 0; i < widget.maxLength; i++) { + width += widget.pinBoxWidth; + if (i == 0) { + width += widget.pinBoxOuterPadding.left; + } else if (i + 1 == widget.maxLength) { + width += widget.pinBoxOuterPadding.right; + } else { + width += widget.pinBoxOuterPadding.left; + } + } + return width; + } + + @override + void dispose() { + if (widget.focusNode == null) { + focusNode.dispose(); + } else { + focusNode.removeListener(_focusListener); + } + _highlightAnimationController.dispose(); + widget.controller?.removeListener(_controllerListener); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _otpTextInput(), + _touchPinBoxRow(), + ], + ); + } + + Widget _touchPinBoxRow() { + return widget.hideDefaultKeyboard + ? _pinBoxRow(context) + : GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (hasFocus) { + FocusScope.of(context).requestFocus(FocusNode()); + Future.delayed(Duration(milliseconds: 100), () { + FocusScope.of(context).requestFocus(focusNode); + }); + } else { + FocusScope.of(context).requestFocus(focusNode); + } + }, + child: _pinBoxRow(context), + ); + } + + Widget _otpTextInput() { + var transparentBorder = OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + width: 0.0, + ), + ); + return SizedBox( + width: _width, + height: widget.pinBoxHeight, + child: TextField( + autofocus: !kIsWeb ? widget.autoFocus : false, + enableInteractiveSelection: false, + focusNode: focusNode, + controller: widget.controller, + keyboardType: widget.keyboardType, + inputFormatters: widget.keyboardType == TextInputType.number ? [FilteringTextInputFormatter.digitsOnly] : null, + // Enable SMS autofill + autofillHints: const [AutofillHints.oneTimeCode], + style: TextStyle( + height: 0.1, + color: Colors.transparent, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(0), + focusedErrorBorder: transparentBorder, + errorBorder: transparentBorder, + disabledBorder: transparentBorder, + enabledBorder: transparentBorder, + focusedBorder: transparentBorder, + counterText: null, + counterStyle: null, + helperStyle: TextStyle( + height: 0.0, + color: Colors.transparent, + ), + labelStyle: TextStyle(height: 0.1), + fillColor: Colors.transparent, + border: InputBorder.none, + ), + cursorColor: Colors.transparent, + showCursor: false, + maxLength: widget.maxLength, + onChanged: _onTextChanged, + ), + ); + } + + void _onTextChanged(text) { + var onTextChanged = widget.onTextChanged; + if (onTextChanged != null) { + onTextChanged(text); + } + setState(() { + this.text = text; + if (text.length >= currentIndex) { + for (int i = currentIndex; i < text.length; i++) { + strList[i] = text[i]; + } + } + currentIndex = text.length; + }); + if (text.length == widget.maxLength) { + FocusScope.of(context).requestFocus(FocusNode()); + var onDone = widget.onDone; + if (onDone != null) { + onDone(text); + } + } + } + + Widget _pinBoxRow(BuildContext context) { + _calculateStrList(); + List pinCodes = List.generate(widget.maxLength, (int i) { + return _buildPinCode(i, context); + }); + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: pinCodes, + ); + } + + Widget _buildPinCode(int i, BuildContext context) { + Color borderColor; + Color pinBoxColor; + + // Determine if OTP is complete + bool isComplete = text.length == widget.maxLength; + + if (widget.hasError) { + borderColor = widget.errorBorderColor; + pinBoxColor = widget.pinBoxColor; + } else if (isComplete) { + borderColor = Colors.transparent; + pinBoxColor = AppColors.successColor; + } else if (i < text.length) { + borderColor = Colors.transparent; + pinBoxColor = AppColors.blackBgColor; + } else { + borderColor = Colors.transparent; + pinBoxColor = widget.pinBoxColor; + } + + EdgeInsets insets; + if (i == 0) { + insets = EdgeInsets.only( + left: 0, + top: widget.pinBoxOuterPadding.top, + right: widget.pinBoxOuterPadding.right, + bottom: widget.pinBoxOuterPadding.bottom, + ); + } else if (i == strList.length - 1) { + insets = EdgeInsets.only( + left: widget.pinBoxOuterPadding.left, + top: widget.pinBoxOuterPadding.top, + right: 0, + bottom: widget.pinBoxOuterPadding.bottom, + ); + } else { + insets = widget.pinBoxOuterPadding; + } + return Container( + key: ValueKey("container$i"), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0), + margin: insets, + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + width: widget.pinBoxBorderWidth, + ), + color: pinBoxColor, + borderRadius: BorderRadius.circular(widget.pinBoxRadius), + ), + width: widget.pinBoxWidth, + height: widget.pinBoxHeight, + child: _animatedTextBox(strList[i], i), + ); + } + + Widget _animatedTextBox(String text, int i) { + if (widget.pinTextAnimatedSwitcherTransition != null) { + return AnimatedSwitcher( + duration: widget.pinTextAnimatedSwitcherDuration, + transitionBuilder: widget.pinTextAnimatedSwitcherTransition ?? + (Widget child, Animation animation) { + return child; + }, + child: Text( + text, + key: ValueKey("$text$i"), + style: widget.pinTextStyle, + ), + ); + } else { + return Text( + text, + key: ValueKey("${strList[i]}$i"), + style: widget.pinTextStyle, + ); + } + } +} + class OTPVerificationScreen extends StatefulWidget { final String phoneNumber; final Function(int code) checkActivationCode; @@ -25,36 +407,21 @@ class OTPVerificationScreen extends StatefulWidget { class _OTPVerificationScreenState extends State { final int _otpLength = 4; - late final List _controllers; - late final List _focusNodes; + late TextEditingController _otpController; Timer? _resendTimer; int _resendTime = 60; - bool _isOtpComplete = false; @override void initState() { super.initState(); - _controllers = List.generate(_otpLength, (_) => TextEditingController()); - _focusNodes = List.generate(_otpLength, (_) => FocusNode()); + _otpController = TextEditingController(); _startResendTimer(); - - // Focus the first field once the screen is built - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_focusNodes.isNotEmpty) { - FocusScope.of(context).requestFocus(_focusNodes[0]); - } - }); } @override void dispose() { - for (final c in _controllers) { - c.dispose(); - } - for (final f in _focusNodes) { - f.dispose(); - } + _otpController.dispose(); _resendTimer?.cancel(); super.dispose(); } @@ -69,25 +436,23 @@ class _OTPVerificationScreenState extends State { }); } - void _onOtpChanged(int index, String value) { - if (value.length == 1 && index < _otpLength - 1) { - _focusNodes[index + 1].requestFocus(); - } else if (value.isEmpty && index > 0) { - _focusNodes[index - 1].requestFocus(); + void _onOtpChanged(String value) { + // Handle clipboard paste or programmatic input + if (value.length > 1) { + String? otp = _extractOtpFromText(value); + if (otp != null) { + autoFillOtp(otp); + return; + } } - _checkOtpCompletion(); + + // The OTPWidget will automatically call onDone when complete + // This method can be used for any additional logic on text change } - void _checkOtpCompletion() { - final isComplete = _controllers.every((c) => c.text.isNotEmpty); - - if (isComplete != _isOtpComplete) { - setState(() => _isOtpComplete = isComplete); - - if (isComplete) { - _verifyOtp(); - } - } + void _onOtpCompleted(String otp) { + debugPrint('OTP Completed: $otp'); + widget.checkActivationCode(int.parse(otp)); } void _resendOtp() { @@ -95,8 +460,7 @@ class _OTPVerificationScreenState extends State { setState(() => _resendTime = 60); _startResendTimer(); autoFillOtp("1234"); - - // call resend API here + widget.onResendOTPPressed(widget.phoneNumber); } } @@ -105,6 +469,68 @@ class _OTPVerificationScreenState extends State { return phone.length > 4 ? '05xxxxxx${phone.substring(phone.length - 2)}' : phone; } + /// Extract OTP from text using multiple patterns + String? _extractOtpFromText(String text) { + // Pattern 1: Find 4-6 consecutive digits + RegExp digitPattern = RegExp(r'\b\d{4,6}\b'); + Match? match = digitPattern.firstMatch(text); + + if (match != null) { + String digits = match.group(0)!; + if (digits.length >= _otpLength) { + return digits.substring(0, _otpLength); + } + } + + // Pattern 2: Find digits separated by spaces or special characters + String cleanedText = text.replaceAll(RegExp(r'[^\d]'), ''); + if (cleanedText.length >= _otpLength) { + return cleanedText.substring(0, _otpLength); + } + + return null; + } + + /// Paste OTP from clipboard + Future _pasteFromClipboard() async { + try { + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + String clipboardText = data.text!; + String? otp = _extractOtpFromText(clipboardText); + + if (otp != null) { + autoFillOtp(otp); + + // Show feedback to user + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('OTP pasted: $otp'), + duration: const Duration(seconds: 2), + backgroundColor: AppColors.successColor, + ), + ); + } else { + // Show error if no valid OTP found + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No valid OTP found in clipboard'), + duration: Duration(seconds: 2), + ), + ); + } + } + } catch (e) { + debugPrint('Error pasting from clipboard: $e'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to paste from clipboard'), + duration: Duration(seconds: 2), + ), + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -134,56 +560,35 @@ class _OTPVerificationScreenState extends State { ), SizedBox(height: 40.h), - // OTP Input Fields - SizedBox( - height: 100, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: List.generate(_otpLength, (index) { - return ValueListenableBuilder( - valueListenable: _controllers[index], - builder: (context, value, _) { - final hasText = value.text.isNotEmpty; - - return AnimatedContainer( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - width: 70.h, - margin: EdgeInsets.symmetric(horizontal: 4.h), - decoration: RoundedRectangleBorder() - .toSmoothCornerDecoration(color: _isOtpComplete ? AppColors.successColor : (hasText ? AppColors.blackBgColor : AppColors.whiteColor), borderRadius: 16), - child: Center( - child: TextField( - controller: _controllers[index], - focusNode: _focusNodes[index], - textAlign: TextAlign.center, - keyboardType: TextInputType.number, - maxLength: 1, - style: TextStyle( - fontSize: 40.fSize, - fontWeight: FontWeight.bold, - color: AppColors.whiteColor, - ), - decoration: InputDecoration( - counterText: '', - filled: true, - fillColor: Colors.transparent, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - borderSide: BorderSide.none, - ), - ), - onChanged: (v) => _onOtpChanged(index, v), - ), - ), - ); - }, - ); - }), + // OTP Input Fields using new OTPWidget + Center( + child: AutofillGroup( + child: OTPWidget( + maxLength: _otpLength, + controller: _otpController, + pinBoxWidth: 75.h, + pinBoxHeight: 100.h, + autoFocus: true, + pinBoxRadius: 16, + pinBoxBorderWidth: 0, + pinBoxOuterPadding: EdgeInsets.symmetric(horizontal: 4.h), + defaultBorderColor: Colors.transparent, + textBorderColor: Colors.transparent, + errorBorderColor: AppColors.primaryRedColor, + pinBoxColor: AppColors.whiteColor, + pinTextStyle: TextStyle( + fontSize: 50.fSize, + fontWeight: FontWeight.bold, + color: AppColors.whiteColor, + ), + onTextChanged: _onOtpChanged, + onDone: _onOtpCompleted, + ), ), ), - const SizedBox(height: 32), + const SizedBox(height: 16), + // Resend OTP Row( @@ -215,30 +620,50 @@ class _OTPVerificationScreenState extends State { ); } - void _verifyOtp() { - final otp = _controllers.map((c) => c.text).join(); - debugPrint('Verifying OTP: $otp'); - - widget.checkActivationCode(int.parse(otp)); - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text('Verifying OTP: $otp')), - // ); - - // Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => RegisterNewStep2(null, {"nationalID": "12345678654321"}))); - } - /// Auto fill OTP into text fields void autoFillOtp(String otp) { if (otp.length != _otpLength) return; + + // Clear any existing text first + _otpController.clear(); + + // Add a small delay to ensure the UI is updated + Future.delayed(const Duration(milliseconds: 50), () { + _otpController.text = otp; + // Move cursor to the end + _otpController.selection = TextSelection.fromPosition( + TextPosition(offset: otp.length), + ); + }); + } - for (int i = 0; i < _otpLength; i++) { - _controllers[i].text = otp[i]; - } + /// Clear OTP fields + void clearOtp() { + _otpController.clear(); + } + + /// Get current OTP value + String getCurrentOtp() { + return _otpController.text; + } - // Move focus to the last field - _focusNodes[_otpLength - 1].requestFocus(); + /// Check if OTP is complete + bool isOtpComplete() { + return _otpController.text.length == _otpLength; + } - // Trigger completion check and color update - _checkOtpCompletion(); + /// Simulate SMS received with OTP (for testing purposes) + void simulateSMSReceived(String otp) { + if (otp.length == _otpLength && RegExp(r'^\d+$').hasMatch(otp)) { + autoFillOtp(otp); + // Show a brief indicator that SMS was detected + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('OTP detected from SMS: $otp'), + duration: const Duration(seconds: 2), + backgroundColor: AppColors.successColor, + ), + ); + } } } diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index 0b463cc..2a8c744 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; import 'package:hmg_patient_app_new/extensions/context_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; @@ -28,13 +29,17 @@ class LoginScreen extends StatefulWidget { } class LoginScreenState extends State { + late FocusNode _nationalIdFocusNode; + @override void initState() { super.initState(); + _nationalIdFocusNode = FocusNode(); } @override void dispose() { + _nationalIdFocusNode.dispose(); super.dispose(); } @@ -48,12 +53,14 @@ class LoginScreenState extends State { Navigator.of(context).pop(); }, onLanguageChanged: (String value) { - // context.setLocale(value == 'en' ? Locale('ar', 'SA') : Locale('en', 'US')); + context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA')); }, ), body: GestureDetector( onTap: () { - FocusScope.of(context).unfocus(); // Dismiss the keyboard when tapping outside + // Dismiss the keyboard and unfocus any focused widget when tapping outside + _nationalIdFocusNode.unfocus(); + FocusScope.of(context).unfocus(); }, child: SingleChildScrollView( child: Padding( @@ -70,6 +77,7 @@ class LoginScreenState extends State { labelText: "${LocaleKeys.nationalId.tr()} / ${LocaleKeys.fileNo.tr()}", hintText: "xxxxxxxxx", controller: authVm.nationalIdController, + focusNode: _nationalIdFocusNode, keyboardType: TextInputType.number, isEnable: true, prefix: null, @@ -80,7 +88,7 @@ class LoginScreenState extends State { padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.h), leadingIcon: AppAssets.student_card, errorMessage: "Please enter a valid national ID or file number", - hasError: true, + hasError: false, ), SizedBox(height: 16.h), // Adjusted to sizer unit (approx 16px) CustomButton( @@ -88,20 +96,16 @@ class LoginScreenState extends State { icon: AppAssets.login1, iconColor: Colors.white, onPressed: () { - showLoginModelSheet(context: context, phoneNumberController: authVm.phoneNumberController, authViewModel: authVm); - // if (nationIdController.text.isNotEmpty) { + _nationalIdFocusNode.unfocus(); + FocusScope.of(context).unfocus(); - // } else { - // showBottomSheet( - // child: ExceptionBottomSheet( - // message: TranslationBase.of(context).pleaseEnterNationalIdOrFileNo, - // showCancel: false, - // onOkPressed: () { - // Navigator.of(context).pop(); - // }, - // ), - // ); - // } + if (ValidationUtils.isValidatedId( + nationalId: authVm.nationalIdController.text, + onOkPress: () { + Navigator.of(context).pop(); + })) { + showLoginModelSheet(context: context, phoneNumberController: authVm.phoneNumberController, authViewModel: authVm); + } }, ), SizedBox(height: 10.h), // Adjusted to sizer unit (approx 14px) @@ -121,11 +125,10 @@ class LoginScreenState extends State { TextSpan( text: LocaleKeys.registernow.tr(), style: context.dynamicTextStyle( - color: AppColors.primaryRedColor, - fontSize: 14.fSize, // Adjusted to sizer unit - height: 26 / 16, // Ratio - fontWeight: FontWeight.w500, - ), + color: AppColors.primaryRedColor, + fontSize: 14.fSize, // Adjusted to sizer unit + height: 26 / 16, // Ratio + fontWeight: FontWeight.w500), recognizer: TapGestureRecognizer() ..onTap = () { Navigator.of(context).push( @@ -172,7 +175,13 @@ class LoginScreenState extends State { child: CustomButton( text: LocaleKeys.sendOTPSMS.tr(), onPressed: () async { - await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); + if (ValidationUtils.isValidatePhone( + phoneNumber: phoneNumberController!.text, + onOkPress: () { + Navigator.of(context).pop(); + })) { + await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); + } }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedBorderColor, @@ -195,9 +204,13 @@ class LoginScreenState extends State { child: CustomButton( text: LocaleKeys.sendOTPWHATSAPP.tr(), onPressed: () async { - log("phoneNumberController: ${phoneNumberController == null}"); - log("phoneNumberControllerVa: ${phoneNumberController?.text}"); - await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); + if (ValidationUtils.isValidatePhone( + phoneNumber: phoneNumberController!.text, + onOkPress: () { + Navigator.of(context).pop(); + })) { + await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); + } }, backgroundColor: Colors.white, borderColor: AppColors.borderOnlyColor, diff --git a/lib/presentation/authentication/register.dart b/lib/presentation/authentication/register.dart index efb0a18..8ec988d 100644 --- a/lib/presentation/authentication/register.dart +++ b/lib/presentation/authentication/register.dart @@ -2,11 +2,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/app_state.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; @@ -26,19 +25,25 @@ class RegisterNew extends StatefulWidget { } class _RegisterNew extends State { + late FocusNode _nationalIdFocusNode; + late FocusNode _dobFocusNode; + @override void initState() { super.initState(); + _nationalIdFocusNode = FocusNode(); + _dobFocusNode = FocusNode(); } @override void dispose() { + _nationalIdFocusNode.dispose(); + _dobFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - AppState appState = getIt.get(); AuthenticationViewModel authVm = context.read(); return Scaffold( @@ -48,11 +53,14 @@ class _RegisterNew extends State { Navigator.of(context).pop(); }, onLanguageChanged: (String value) { - // context.setLocale(value == 'en' ? Locale('ar', 'SA') : Locale('en', 'US')); + context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA')); }, ), body: GestureDetector( onTap: () { + // Dismiss keyboard and unfocus all input fields + _nationalIdFocusNode.unfocus(); + _dobFocusNode.unfocus(); FocusScope.of(context).unfocus(); }, child: ScrollConfiguration( @@ -90,6 +98,7 @@ class _RegisterNew extends State { labelText: LocaleKeys.nationalIdNumber.tr(), hintText: "xxxxxxxxx", controller: authVm.nationalIdController, + focusNode: _nationalIdFocusNode, isEnable: true, prefix: null, isAllowRadius: true, @@ -104,6 +113,7 @@ class _RegisterNew extends State { labelText: LocaleKeys.dob.tr(), hintText: "11 July, 1994", controller: authVm.dobController, + focusNode: _dobFocusNode, isEnable: true, prefix: null, isAllowRadius: true, @@ -156,7 +166,21 @@ class _RegisterNew extends State { text: "Register", icon: AppAssets.note_edit, onPressed: () { - showRegisterModel(context: context, authVM: authVm); + // Dismiss keyboard before proceeding + _nationalIdFocusNode.unfocus(); + _dobFocusNode.unfocus(); + FocusScope.of(context).unfocus(); + + if (ValidationUtils.isValidatedId( + nationalId: authVm.nationalIdController.text, + selectedCountry: authVm.selectedCountrySignup, + isTermsAccepted: authVm.isTermsAccepted, + dob: authVm.dobController.text, + onOkPress: () { + Navigator.of(context).pop(); + })) { + showRegisterModel(context: context, authVM: authVm); + } }, ), SizedBox(height: 14), @@ -216,13 +240,24 @@ class _RegisterNew extends State { isEnableCountryDropdown: false, onCountryChange: authVM.onCountryChange, onChange: authVM.onPhoneNumberChange, + autoFocus: true, buttons: [ Padding( padding: const EdgeInsets.only(bottom: 10), child: CustomButton( text: LocaleKeys.sendOTPSMS.tr(), onPressed: () async { - await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.sms); + // Dismiss keyboard before validation + FocusScope.of(context).unfocus(); + + if (ValidationUtils.isValidatePhone( + phoneNumber: authVM.phoneNumberController.text, + onOkPress: () { + Navigator.of(context).pop(); + }, + )) { + await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.sms); + } }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedBorderColor, @@ -245,7 +280,17 @@ class _RegisterNew extends State { child: CustomButton( text: LocaleKeys.sendOTPWHATSAPP.tr(), onPressed: () async { - await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.whatsapp); + // Dismiss keyboard before validation + FocusScope.of(context).unfocus(); + + if (ValidationUtils.isValidatePhone( + phoneNumber: authVM.phoneNumberController.text, + onOkPress: () { + Navigator.of(context).pop(); + }, + )) { + await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.whatsapp); + } }, backgroundColor: AppColors.whiteColor, borderColor: AppColors.borderOnlyColor, diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index d711402..9f55c85 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -42,7 +42,7 @@ class _SavedLogin extends State { authVm.nationalIdController.text = appState.getSelectDeviceByImeiRespModelElement!.identificationNo!; if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) { - authVm.loginWithFingerPrintFace((){}); + authVm.loginWithFingerPrintFace(() {}); } super.initState(); @@ -101,7 +101,7 @@ class _SavedLogin extends State { text: "${LocaleKeys.loginBy.tr()} ${loginType.displayName}", onPressed: () { if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) { - authVm.loginWithFingerPrintFace((){}); + authVm.loginWithFingerPrintFace(() {}); } else { // int? val = loginType.toInt; authVm.checkUserAuthentication(otpTypeEnum: loginType == LoginTypeEnum.sms ? OTPTypeEnum.sms : OTPTypeEnum.whatsapp); @@ -114,8 +114,9 @@ class _SavedLogin extends State { fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(0, 10, 0, 10), - icon: getTypeIcons(loginType.toInt), //loginType == LoginTypeEnum.sms ? AppAssets.sms :AppAssets.whatsapp, - iconColor: loginType != LoginTypeEnum.whatsapp ? Colors.white: null , + icon: getTypeIcons(loginType.toInt), + //loginType == LoginTypeEnum.sms ? AppAssets.sms :AppAssets.whatsapp, + iconColor: loginType != LoginTypeEnum.whatsapp ? Colors.white : null, ), ), ], @@ -226,7 +227,7 @@ class _SavedLogin extends State { iconColor: null, onPressed: () { if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) { - authVm.loginWithFingerPrintFace((){}); + authVm.loginWithFingerPrintFace(() {}); } else { loginType = LoginTypeEnum.whatsapp; int? val = loginType.toInt; @@ -272,11 +273,12 @@ class _SavedLogin extends State { width: MediaQuery.of(context).size.width * 0.05, ), Expanded( - child: Container( + child: SizedBox( height: 56, child: CustomButton( text: LocaleKeys.switchAccount.tr(), - onPressed: () { + onPressed: () async { + await authVm.clearDefaultInputValues(); Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (BuildContext context) => LoginScreen()), ); diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 9ba939b..29aee3d 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -1,10 +1,28 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/bottomsheet/exception_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; abstract class DialogService { Future showErrorBottomSheet({required String message, Function()? onOkPressed}); + + Future showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}); + + Future showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}); + + Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}); +// TODO : Need to be Fixed showPhoneNumberPickerSheet ( From Login ADn Signup Bottom Sheet Move Here } class DialogServiceImp implements DialogService { @@ -23,9 +41,166 @@ class DialogServiceImp implements DialogService { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), - builder: (_) => _ErrorBottomSheet(message: message, onOkPressed: onOkPressed), + builder: (_) => _ErrorBottomSheet(message: message, onOkPressed: onOkPressed ?? () {}), + ); + } + + @override + Future showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + + await showModalBottomSheet( + context: context, + isScrollControlled: false, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (_) => ExceptionBottomSheet( + message: message, + showCancel: onCancelPressed != null ? true : false, + onOkPressed: onOkPressed, + onCancelPressed: () { + if (onCancelPressed != null) { + Navigator.of(context).pop(); + } + }, + ), ); } + + @override + Future showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + showCommonBottomSheetWithoutHeight(context, title: label ?? "", child: exceptionBottomSheetWidget(context: context, message: message, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed), + callBackFunc: () { + }); + } + + @override + Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + showCommonBottomSheetWithoutHeight(context, + title: label ?? "", child: showPhoneNumberPickerWidget(context: context, message: message, onSMSPress: onSMSPress, onWhatsappPress: onWhatsappPress), callBackFunc: () {}); + } +} + +Widget exceptionBottomSheetWidget({required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) { + return Column( + children: [ + (message ?? "").toText16(isBold: false, color: AppColors.textColor), + SizedBox(height: 10.h), + SizedBox(height: 24.h), + if (onOkPressed != null && onCancelPressed != null) + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.cancel.tr(), + onPressed: () { + Navigator.of(context).pop(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + icon: AppAssets.cancel, + iconColor: AppColors.primaryRedColor, + ), + ), + SizedBox(width: 5.h), + Expanded( + child: CustomButton( + text: onCancelPressed != null ? LocaleKeys.confirm.tr() : LocaleKeys.ok.tr(), + onPressed: onOkPressed, + backgroundColor: AppColors.bgGreenColor, + borderColor: AppColors.bgGreenColor, + textColor: Colors.white, + icon: AppAssets.confirm, + ), + ), + ], + ), + if (onOkPressed != null && onCancelPressed == null) + Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: CustomButton( + text: LocaleKeys.ok.tr(), + onPressed: (onOkPressed != null && onCancelPressed == null) + ? () { + Navigator.of(context).pop(); + } + : onOkPressed, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedBorderColor, + textColor: Colors.white, + icon: AppAssets.confirm, + ), + ), + ], + ); +} + +Widget showPhoneNumberPickerWidget({required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) { + return StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { + AuthenticationViewModel authViewModel = context.read(); + return Column( + children: [ + (message ?? "").toText16(isBold: false, color: AppColors.textColor), + SizedBox(height: 10.h), + Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: CustomButton( + text: LocaleKeys.sendOTPSMS.tr(), + onPressed: onSMSPress, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedBorderColor, + textColor: AppColors.whiteColor, + icon: AppAssets.message, + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.h), + child: LocaleKeys.oR.tr().toText16(color: AppColors.textColor), + ), + ], + ), + Padding( + padding: EdgeInsets.only(bottom: 10.h, top: 10.h), + child: CustomButton( + text: LocaleKeys.sendOTPWHATSAPP.tr(), + onPressed: onWhatsappPress, + backgroundColor: Colors.white, + borderColor: AppColors.borderOnlyColor, + textColor: AppColors.textColor, + icon: AppAssets.whatsapp, + iconColor: null, + ), + ), + ], + ); + //return Padding( + // padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + // child: SingleChildScrollView( + // child: GenericBottomSheet( + // countryCode: authViewModel.selectedCountrySignup.countryCode, + // initialPhoneNumber: "", + // textController: authViewModel.phoneNumberController, + // isEnableCountryDropdown: true, + // onCountryChange: authViewModel.onCountryChange, + // onChange: authViewModel.onPhoneNumberChange, + // buttons: [ + // + // ], + // ), + // ), + // ); + }); } class _ErrorBottomSheet extends StatelessWidget { @@ -71,7 +246,7 @@ class _ErrorBottomSheet extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), ), - child: const Text("OK", style: TextStyle(color: Colors.white)).onPress((){ + child: const Text("OK", style: TextStyle(color: Colors.white)).onPress(() { Navigator.of(context).pop(); }), ), diff --git a/lib/services/error_handler_service.dart b/lib/services/error_handler_service.dart index 092cf38..d4c15f2 100644 --- a/lib/services/error_handler_service.dart +++ b/lib/services/error_handler_service.dart @@ -3,12 +3,13 @@ import 'dart:io'; import 'package:hmg_patient_app_new/core/exceptions/api_exception.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/loading_utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; abstract class ErrorHandlerService { - Future handleError({required Failure failure, Function() onOkPressed}); + Future handleError({required Failure failure, Function() onOkPressed, Function(Failure)? onUnHandledFailure}); } class ErrorHandlerServiceImp implements ErrorHandlerService { @@ -23,7 +24,7 @@ class ErrorHandlerServiceImp implements ErrorHandlerService { }); @override - Future handleError({required Failure failure, Function()? onOkPressed}) async { + Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure}) async { if (failure is APIException) { loggerService.errorLogs("API Exception: ${failure.message}"); } else if (failure is ServerFailure) { @@ -44,9 +45,14 @@ class ErrorHandlerServiceImp implements ErrorHandlerService { } else if (failure is InvalidCredentials) { loggerService.errorLogs("Invalid Credentials : ${failure.message}"); await _showDialog(failure, title: "Unknown Error"); + } else if (failure is UserIntimationFailure) { + if (onUnHandledFailure != null) { + onUnHandledFailure(failure); + } else { + await _showDialog(failure, title: "Error", onOkPressed: onOkPressed); + } } else { loggerService.errorLogs("Unhandled failure type: $failure"); - await _showDialog(failure, title: "Error", onOkPressed: onOkPressed); } } @@ -55,6 +61,7 @@ class ErrorHandlerServiceImp implements ErrorHandlerService { if (LoadingUtils.isLoading) { LoadingUtils.hideFullScreenLoader(); } + await dialogService.showErrorBottomSheet(message: failure.message, onOkPressed: onOkPressed); } } diff --git a/lib/widgets/appbar/app_bar_widget.dart b/lib/widgets/appbar/app_bar_widget.dart index 4bc08a9..1b6b8f1 100644 --- a/lib/widgets/appbar/app_bar_widget.dart +++ b/lib/widgets/appbar/app_bar_widget.dart @@ -32,11 +32,11 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { title: Padding( padding: EdgeInsets.symmetric(horizontal: 10.h), child: Row( - mainAxisAlignment: MainAxisAlignment.start, + // mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( child: Align( - alignment: Alignment.centerLeft, + alignment: context.locale.languageCode == "ar" ? Alignment.centerRight : Alignment.centerLeft, child: GestureDetector( onTap: onBackPressed, child: Utils.buildSvgWithAssets(icon: AppAssets.arrow_back, width: 32.h, height: 32.h), @@ -53,7 +53,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { if (!hideLogoAndLang) Expanded( child: Align( - alignment: Alignment.centerRight, + alignment: context.locale.languageCode == "ar" ? Alignment.centerLeft : Alignment.centerRight, child: LanguageSelector( currentLanguage: context.locale.languageCode, showOnlyIcon: false, diff --git a/lib/widgets/bottomsheet/generic_bottom_sheet.dart b/lib/widgets/bottomsheet/generic_bottom_sheet.dart index 74062ef..4c7f5af 100644 --- a/lib/widgets/bottomsheet/generic_bottom_sheet.dart +++ b/lib/widgets/bottomsheet/generic_bottom_sheet.dart @@ -22,6 +22,7 @@ class GenericBottomSheet extends StatefulWidget { final bool isEnableCountryDropdown; final bool isFromSavedLogin; Function(String?)? onChange; + final bool autoFocus; // FocusNode myFocusNode; @@ -36,6 +37,7 @@ class GenericBottomSheet extends StatefulWidget { this.isEnableCountryDropdown = false, this.isFromSavedLogin = false, this.onChange, + this.autoFocus = false, // required this.myFocusNode }); @@ -44,17 +46,28 @@ class GenericBottomSheet extends StatefulWidget { } class _GenericBottomSheetState extends State { + late FocusNode _textFieldFocusNode; + @override void initState() { super.initState(); + _textFieldFocusNode = FocusNode(); if (!widget.isForEmail && widget.textController != null) { widget.textController!.text = widget.initialPhoneNumber ?? ""; } + + // Auto focus the text field if specified + if (widget.autoFocus && widget.textController != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _textFieldFocusNode.requestFocus(); + }); + } } @override void dispose() { + _textFieldFocusNode.dispose(); super.dispose(); } @@ -65,6 +78,8 @@ class _GenericBottomSheetState extends State { bottom: Platform.isIOS ? false : true, child: GestureDetector( onTap: () { + // Dismiss keyboard and unfocus text field + _textFieldFocusNode.unfocus(); FocusScope.of(context).unfocus(); }, child: Directionality( @@ -90,6 +105,9 @@ class _GenericBottomSheetState extends State { : LocaleKeys.enterPhoneNumber.tr().toText24()), InkWell( onTap: () { + // Dismiss keyboard before closing + _textFieldFocusNode.unfocus(); + FocusScope.of(context).unfocus(); Navigator.of(context).pop(); }, child: Padding( @@ -115,6 +133,8 @@ class _GenericBottomSheetState extends State { labelText: widget.isForEmail ? LocaleKeys.email : LocaleKeys.phoneNumber, hintText: widget.isForEmail ? "demo@gmail.com" : "5xxxxxxxx", controller: widget.textController!, + focusNode: _textFieldFocusNode, + autoFocus: widget.autoFocus, padding: EdgeInsets.all(8.h), keyboardType: widget.isForEmail ? TextInputType.emailAddress : TextInputType.number, onChange: (value) { diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index cc0c03b..b88c922 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -126,25 +126,27 @@ void showCommonBottomSheetWithoutHeight( top: false, left: false, right: false, - child:isCloseButtonVisible ? Container( - padding: EdgeInsets.only(left: 24, top: 24, right: 24, bottom: 12), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.bottomSheetBgColor, borderRadius: 24.h), - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: 16.h, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, + child: isCloseButtonVisible + ? Container( + padding: EdgeInsets.only(left: 24, top: 24, right: 24, bottom: 12), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.bottomSheetBgColor, borderRadius: 24.h), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 16.h, children: [ - if (title.isNotEmpty) title.toText20(weight: FontWeight.w600).expanded, - Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, iconColor: Color(0xff2B353E)).onPress(() { - Navigator.of(context).pop(); - }), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + title.toText20(weight: FontWeight.w600).expanded, + Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, iconColor: Color(0xff2B353E)).onPress(() { + Navigator.of(context).pop(); + }), + ], + ), + child, ], - ), - child, - ], - )) : child, + )) + : child, ); }).then((value) { callBackFunc(); diff --git a/lib/widgets/dropdown/country_dropdown_widget.dart b/lib/widgets/dropdown/country_dropdown_widget.dart index 4e41c31..4507423 100644 --- a/lib/widgets/dropdown/country_dropdown_widget.dart +++ b/lib/widgets/dropdown/country_dropdown_widget.dart @@ -1,6 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -60,6 +62,7 @@ class _CustomCountryDropdownState extends State { @override Widget build(BuildContext context) { + AppState appState = getIt.get(); return Container( height: 40.h, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 10.h), @@ -120,7 +123,11 @@ class _CustomCountryDropdownState extends State { ), if (!widget.isFromBottomSheet) Text( - selectedCountry != null ? selectedCountry!.displayName : "Select Country", + selectedCountry != null + ? appState.getLanguageCode() == "ar" + ? selectedCountry!.nameArabic + : selectedCountry!.displayName + : "Select Country", style: TextStyle(fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, letterSpacing: -0.2), ), ], @@ -132,7 +139,7 @@ class _CustomCountryDropdownState extends State { if (textFocusNode.hasFocus) { textFocusNode.unfocus(); } - + AppState appState = getIt.get(); RenderBox renderBox = context.findRenderObject() as RenderBox; Offset offset = renderBox.localToGlobal(Offset.zero); @@ -172,7 +179,9 @@ class _CustomCountryDropdownState extends State { children: [ Utils.buildSvgWithAssets(icon: country.iconPath, width: 38.h, height: 38.h), if (!widget.isFromBottomSheet) SizedBox(width: 12.h), - if (!widget.isFromBottomSheet) Text(country.displayName, style: TextStyle(fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, letterSpacing: -0.2)), + if (!widget.isFromBottomSheet) + Text(appState.getLanguageCode() == "ar" ? country.nameArabic : country.displayName, + style: TextStyle(fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, letterSpacing: -0.2)), ], ), ), diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 44e1327..66c3877 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -75,31 +75,32 @@ class TextInputWidget extends StatelessWidget { final FocusNode _focusNode = FocusNode(); - KeyboardActionsConfig get _keyboardActionsConfig { - return KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.ALL, - keyboardBarColor: const Color(0xFFCAD1D9), //Apple keyboard color - actions: [ - KeyboardActionsItem( - focusNode: focusNode ?? _focusNode, - toolbarButtons: [ - (node) { - return GestureDetector( - onTap: () => node.unfocus(), - child: Container( - padding: const EdgeInsets.all(12.0), - child: "Done".toText16(weight: FontWeight.w500, color: AppColors.infoColor), - ), - ); - } - ], - ), - ], - ); - } + // KeyboardActionsConfig get _keyboardActionsConfig { + // return KeyboardActionsConfig( + // keyboardActionsPlatform: KeyboardActionsPlatform.ALL, + // keyboardBarColor: const Color(0xFFCAD1D9), //Apple keyboard color + // actions: [ + // KeyboardActionsItem( + // focusNode: focusNode ?? _focusNode, + // toolbarButtons: [ + // (node) { + // return GestureDetector( + // onTap: () => node.unfocus(), + // child: Container( + // padding: const EdgeInsets.all(12.0), + // child: "Done".toText16(weight: FontWeight.w500, color: AppColors.infoColor), + // ), + // ); + // } + // ], + // ), + // ], + // ); + // } @override Widget build(BuildContext context) { + AppState appState = getIt.get(); final errorColor = AppColors.primaryRedColor; return Column( mainAxisSize: MainAxisSize.min, @@ -132,7 +133,7 @@ class TextInputWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildLabelText(), + _buildLabelText().paddingOnly(right: (appState.getLanguageCode() == "ar" ? 10 : 0)), _buildTextField(context), ], ), @@ -181,6 +182,7 @@ class TextInputWidget extends StatelessWidget { onTap: () async { bool isGregorian = true; final picked = await showHijriGregBottomSheet(context, + isShowTimeSlots: true, switcherIcon: Utils.buildSvgWithAssets(icon: AppAssets.language, width: 24.h, height: 24.h), language: appState.getLanguageCode()!, initialDate: DateTime.now(), @@ -215,46 +217,42 @@ class TextInputWidget extends StatelessWidget { } Widget _buildTextField(BuildContext context) { - return KeyboardActions( - config: _keyboardActionsConfig, - disableScroll: true, - child: TextField( - enabled: isEnable, - scrollPadding: EdgeInsets.zero, - keyboardType: keyboardType, - controller: controller, - readOnly: isReadOnly, - textAlignVertical: TextAlignVertical.top, - textAlign: TextAlign.left, - textDirection: TextDirection.ltr, - onChanged: onChange, - focusNode: focusNode ?? _focusNode, - autofocus: autoFocus, - textInputAction: TextInputAction.done, - cursorHeight: isWalletAmountInput! ? 40.h : 18.h, - style: TextStyle(fontSize: fontSize!.fSize, height: isWalletAmountInput! ? 1 / 4 : 21 / 14, fontWeight: FontWeight.w500, color: AppColors.textColor, letterSpacing: -0.2), - decoration: InputDecoration( - isDense: true, - hintText: hintText, - hintStyle: TextStyle(fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.2), - prefixIconConstraints: BoxConstraints(minWidth: 45.h), - prefixIcon: prefix == null - ? null - : Text( - "+" + prefix!, - style: TextStyle( - fontSize: 14.fSize, - height: 21 / 14, - fontWeight: FontWeight.w500, - color: Color(0xff2E303A), - letterSpacing: -0.2, - ), + return TextField( + enabled: isEnable, + scrollPadding: EdgeInsets.zero, + keyboardType: keyboardType, + controller: controller, + readOnly: isReadOnly, + textAlignVertical: TextAlignVertical.top, + textAlign: TextAlign.left, + textDirection: TextDirection.ltr, + onChanged: onChange, + focusNode: focusNode ?? _focusNode, + autofocus: autoFocus, + textInputAction: TextInputAction.done, + cursorHeight: isWalletAmountInput! ? 40.h : 18.h, + style: TextStyle(fontSize: fontSize!.fSize, height: isWalletAmountInput! ? 1 / 4 : 21 / 14, fontWeight: FontWeight.w500, color: AppColors.textColor, letterSpacing: -0.2), + decoration: InputDecoration( + isDense: true, + hintText: hintText, + hintStyle: TextStyle(fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.2), + prefixIconConstraints: BoxConstraints(minWidth: 45.h), + prefixIcon: prefix == null + ? null + : Text( + "+" + prefix!, + style: TextStyle( + fontSize: 14.fSize, + height: 21 / 14, + fontWeight: FontWeight.w500, + color: Color(0xff2E303A), + letterSpacing: -0.2, ), - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - ), + ), + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, ), ); } diff --git a/lib/widgets/otp_widget.dart b/lib/widgets/otp_widget.dart deleted file mode 100644 index d606090..0000000 --- a/lib/widgets/otp_widget.dart +++ /dev/null @@ -1,377 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/animation.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; - -typedef OnDone = void Function(String text); - -class ProvidedPinBoxTextAnimation { - static AnimatedSwitcherTransitionBuilder scalingTransition = (child, animation) { - return ScaleTransition( - child: child, - scale: animation, - ); - }; - - static AnimatedSwitcherTransitionBuilder defaultNoTransition = (Widget child, Animation animation) { - return child; - }; -} - -class OTPWidget extends StatefulWidget { - final int maxLength; - late TextEditingController? controller; - - final Color defaultBorderColor; - final Color pinBoxColor; - final double pinBoxBorderWidth; - final double pinBoxRadius; - final bool hideDefaultKeyboard; - - final TextStyle? pinTextStyle; - final double pinBoxHeight; - final double pinBoxWidth; - final OnDone? onDone; - final bool hasError; - final Color errorBorderColor; - final Color textBorderColor; - final Function(String)? onTextChanged; - final bool autoFocus; - final FocusNode? focusNode; - final AnimatedSwitcherTransitionBuilder? pinTextAnimatedSwitcherTransition; - final Duration pinTextAnimatedSwitcherDuration; - final TextDirection textDirection; - final TextInputType keyboardType; - final EdgeInsets pinBoxOuterPadding; - - OTPWidget({ - Key? key, - this.maxLength = 4, - this.controller, - this.pinBoxWidth = 70.0, - this.pinBoxHeight = 70.0, - this.pinTextStyle, - this.onDone, - this.defaultBorderColor = Colors.black, - this.textBorderColor = Colors.black, - this.pinTextAnimatedSwitcherTransition, - this.pinTextAnimatedSwitcherDuration = const Duration(), - this.hasError = false, - this.errorBorderColor = Colors.red, - this.onTextChanged, - this.autoFocus = false, - this.focusNode, - this.textDirection = TextDirection.ltr, - this.keyboardType = TextInputType.number, - this.pinBoxOuterPadding = const EdgeInsets.symmetric(horizontal: 4.0), - this.pinBoxColor = Colors.white, - this.pinBoxBorderWidth = 2.0, - this.pinBoxRadius = 0, - this.hideDefaultKeyboard = false, - }) : super(key: key); - - @override - State createState() { - return OTPWidgetState(); - } -} - -class OTPWidgetState extends State with SingleTickerProviderStateMixin { - late AnimationController _highlightAnimationController; - late FocusNode focusNode; - String text = ""; - int currentIndex = 0; - List strList = []; - bool hasFocus = false; - - @override - void didUpdateWidget(OTPWidget oldWidget) { - super.didUpdateWidget(oldWidget); - focusNode = widget.focusNode ?? focusNode; - - if (oldWidget.maxLength < widget.maxLength) { - setState(() { - currentIndex = text.length; - }); - widget.controller?.text = text; - widget.controller?.selection = TextSelection.collapsed(offset: text.length); - } else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.length > 0 && text.length > widget.maxLength) { - setState(() { - text = text.substring(0, widget.maxLength); - currentIndex = text.length; - }); - widget.controller?.text = text; - widget.controller?.selection = TextSelection.collapsed(offset: text.length); - } - } - - _calculateStrList() { - if (strList.length > widget.maxLength) { - strList.length = widget.maxLength; - } - while (strList.length < widget.maxLength) { - strList.add(""); - } - } - - @override - void initState() { - super.initState(); - focusNode = widget.focusNode ?? FocusNode(); - _highlightAnimationController = AnimationController(vsync: this); - _initTextController(); - _calculateStrList(); - widget.controller!.addListener(_controllerListener); - focusNode.addListener(_focusListener); - } - - void _controllerListener() { - if (mounted == true) { - setState(() { - _initTextController(); - }); - var onTextChanged = widget.onTextChanged; - if (onTextChanged != null) { - onTextChanged(widget.controller?.text ?? ""); - } - } - } - - void _focusListener() { - if (mounted == true) { - setState(() { - hasFocus = focusNode?.hasFocus ?? false; - }); - } - } - - void _initTextController() { - if (widget.controller == null) { - return; - } - strList.clear(); - var text = widget.controller?.text ?? ""; - if (text.isNotEmpty) { - if (text.length > widget.maxLength) { - throw Exception("TextEditingController length exceeded maxLength!"); - } - } - for (var i = 0; i < text.length; i++) { - strList.add(text[i]); - } - } - - double get _width { - var width = 0.0; - for (var i = 0; i < widget.maxLength; i++) { - width += widget.pinBoxWidth; - if (i == 0) { - width += widget.pinBoxOuterPadding.left; - } else if (i + 1 == widget.maxLength) { - width += widget.pinBoxOuterPadding.right; - } else { - width += widget.pinBoxOuterPadding.left; - } - } - return width; - } - - @override - void dispose() { - if (widget.focusNode == null) { - focusNode.dispose(); - } else { - focusNode.removeListener(_focusListener); - } - _highlightAnimationController.dispose(); - widget.controller?.removeListener(_controllerListener); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - _otpTextInput(), - _touchPinBoxRow(), - ], - ); - } - - Widget _touchPinBoxRow() { - return widget.hideDefaultKeyboard - ? _pinBoxRow(context) - : GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (hasFocus) { - FocusScope.of(context).requestFocus(FocusNode()); - Future.delayed(Duration(milliseconds: 100), () { - FocusScope.of(context).requestFocus(focusNode); - }); - } else { - FocusScope.of(context).requestFocus(focusNode); - } - }, - child: _pinBoxRow(context), - ); - } - - Widget _otpTextInput() { - var transparentBorder = OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - width: 0.0, - ), - ); - return Container( - width: _width, - height: widget.pinBoxHeight, - child: TextField( - autofocus: !kIsWeb ? widget.autoFocus : false, - enableInteractiveSelection: false, - focusNode: focusNode, - controller: widget.controller, - keyboardType: widget.keyboardType, - inputFormatters: widget.keyboardType == TextInputType.number ? [FilteringTextInputFormatter.digitsOnly] : null, - style: TextStyle( - height: 0.1, - color: Colors.transparent, - ), - decoration: InputDecoration( - contentPadding: EdgeInsets.all(0), - focusedErrorBorder: transparentBorder, - errorBorder: transparentBorder, - disabledBorder: transparentBorder, - enabledBorder: transparentBorder, - focusedBorder: transparentBorder, - counterText: null, - counterStyle: null, - helperStyle: TextStyle( - height: 0.0, - color: Colors.transparent, - ), - labelStyle: TextStyle(height: 0.1), - fillColor: Colors.transparent, - border: InputBorder.none, - ), - cursorColor: Colors.transparent, - showCursor: false, - maxLength: widget.maxLength, - onChanged: _onTextChanged, - ), - ); - } - - void _onTextChanged(text) { - var onTextChanged = widget.onTextChanged; - if (onTextChanged != null) { - onTextChanged(text); - } - setState(() { - this.text = text; - if (text.length >= currentIndex) { - for (int i = currentIndex; i < text.length; i++) { - strList[i] = text[i]; - } - } - currentIndex = text.length; - }); - if (text.length == widget.maxLength) { - FocusScope.of(context).requestFocus(FocusNode()); - var onDone = widget.onDone; - if (onDone != null) { - onDone(text); - } - } - } - - Widget _pinBoxRow(BuildContext context) { - _calculateStrList(); - List pinCodes = List.generate(widget.maxLength, (int i) { - return _buildPinCode(i, context); - }); - return Row( - children: pinCodes, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - ); - } - - Widget _buildPinCode(int i, BuildContext context) { - Color borderColor; - Color pinBoxColor = widget.pinBoxColor; - - if (widget.hasError) { - borderColor = widget.errorBorderColor; - } else if (i < text.length) { - borderColor = widget.textBorderColor; - } else { - borderColor = widget.defaultBorderColor; - pinBoxColor = widget.pinBoxColor; - } - - EdgeInsets insets; - if (i == 0) { - insets = EdgeInsets.only( - left: 0, - top: widget.pinBoxOuterPadding.top, - right: widget.pinBoxOuterPadding.right, - bottom: widget.pinBoxOuterPadding.bottom, - ); - } else if (i == strList.length - 1) { - insets = EdgeInsets.only( - left: widget.pinBoxOuterPadding.left, - top: widget.pinBoxOuterPadding.top, - right: 0, - bottom: widget.pinBoxOuterPadding.bottom, - ); - } else { - insets = widget.pinBoxOuterPadding; - } - return Container( - key: ValueKey("container$i"), - alignment: Alignment.center, - padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0), - margin: insets, - child: _animatedTextBox(strList[i], i), - decoration: BoxDecoration( - border: Border.all( - color: borderColor, - width: widget.pinBoxBorderWidth, - ), - color: pinBoxColor, - borderRadius: BorderRadius.circular(widget.pinBoxRadius), - ), - width: widget.pinBoxWidth, - height: widget.pinBoxHeight, - ); - } - - Widget _animatedTextBox(String text, int i) { - if (widget.pinTextAnimatedSwitcherTransition != null) { - return AnimatedSwitcher( - duration: widget.pinTextAnimatedSwitcherDuration, - transitionBuilder: widget.pinTextAnimatedSwitcherTransition ?? - (Widget child, Animation animation) { - return child; - }, - child: Text( - text, - key: ValueKey("$text$i"), - style: widget.pinTextStyle, - ), - ); - } else { - return Text( - text, - key: ValueKey("${strList[i]}$i"), - style: widget.pinTextStyle, - ); - } - } -} -- 2.30.2