validations & otp widget & focus & auto fill #32

Merged
Haroon6138 merged 1 commits from dev_aamir into master 2 months ago

@ -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

@ -145,12 +145,6 @@ class AppDependencies {
),
);
getIt.registerLazySingleton<HabibWalletViewModel>(
() => HabibWalletViewModel(
habibWalletRepo: getIt(),
errorHandlerService: getIt(),
),
);
getIt.registerLazySingleton<MedicalFileViewModel>(
() => MedicalFileViewModel(

@ -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'<br\s*\/?>', multiLine: true), '\n').replaceAll(RegExp(r'<\/p>', multiLine: true), '\n').replaceAll(RegExp(r'<divider>', 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('&nbsp;', ' ')
.replaceAll('&amp;', '&')
@ -416,7 +414,7 @@ class Utils {
.replaceAll('&rdquo;', '"')
.replaceAll('&ldquo;', '"');
// 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;
}
}

@ -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);
}
}

@ -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<void> 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<String, dynamic>);
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<String, dynamic>);
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<void> 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<String, dynamic>);
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<void> 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<String, dynamic>);
// 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) {

@ -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<double> 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<StatefulWidget> createState() {
return OTPWidgetState();
}
}
class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixin {
late AnimationController _highlightAnimationController;
late FocusNode focusNode;
String text = "";
int currentIndex = 0;
List<String> 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: <Widget>[
_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 ? <TextInputFormatter>[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<Widget> 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<String>("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<double> animation) {
return child;
},
child: Text(
text,
key: ValueKey<String>("$text$i"),
style: widget.pinTextStyle,
),
);
} else {
return Text(
text,
key: ValueKey<String>("${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<OTPVerificationScreen> {
final int _otpLength = 4;
late final List<TextEditingController> _controllers;
late final List<FocusNode> _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<OTPVerificationScreen> {
});
}
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();
}
void _checkOtpCompletion() {
final isComplete = _controllers.every((c) => c.text.isNotEmpty);
if (isComplete != _isOtpComplete) {
setState(() => _isOtpComplete = isComplete);
// The OTPWidget will automatically call onDone when complete
// This method can be used for any additional logic on text change
}
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<OTPVerificationScreen> {
setState(() => _resendTime = 60);
_startResendTimer();
autoFillOtp("1234");
// call resend API here
widget.onResendOTPPressed(widget.phoneNumber);
}
}
@ -105,6 +469,68 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
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<void> _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<OTPVerificationScreen> {
),
SizedBox(height: 40.h),
// OTP Input Fields
SizedBox(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(_otpLength, (index) {
return ValueListenableBuilder<TextEditingValue>(
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<OTPVerificationScreen> {
);
}
void _verifyOtp() {
final otp = _controllers.map((c) => c.text).join();
debugPrint('Verifying OTP: $otp');
/// Auto fill OTP into text fields
void autoFillOtp(String otp) {
if (otp.length != _otpLength) return;
widget.checkActivationCode(int.parse(otp));
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text('Verifying OTP: $otp')),
// );
// Clear any existing text first
_otpController.clear();
// Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => RegisterNewStep2(null, {"nationalID": "12345678654321"})));
// 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),
);
});
}
/// Auto fill OTP into text fields
void autoFillOtp(String otp) {
if (otp.length != _otpLength) return;
/// Clear OTP fields
void clearOtp() {
_otpController.clear();
}
for (int i = 0; i < _otpLength; i++) {
_controllers[i].text = otp[i];
}
/// 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,
),
);
}
}
}

@ -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<LoginScreen> {
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<LoginScreen> {
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<LoginScreen> {
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<LoginScreen> {
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<LoginScreen> {
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<LoginScreen> {
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<LoginScreen> {
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<LoginScreen> {
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,

@ -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<RegisterNew> {
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<AppState>();
AuthenticationViewModel authVm = context.read<AuthenticationViewModel>();
return Scaffold(
@ -48,11 +53,14 @@ class _RegisterNew extends State<RegisterNew> {
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<RegisterNew> {
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<RegisterNew> {
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<RegisterNew> {
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<RegisterNew> {
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<RegisterNew> {
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,

@ -42,7 +42,7 @@ class _SavedLogin extends State<SavedLogin> {
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<SavedLogin> {
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<SavedLogin> {
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<SavedLogin> {
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<SavedLogin> {
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()),
);

@ -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<void> showErrorBottomSheet({required String message, Function()? onOkPressed});
Future<void> showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed});
Future<void> showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed});
Future<void> 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<void> 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<void> 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<void> 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<AuthenticationViewModel>();
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();
}),
),

@ -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<void> handleError({required Failure failure, Function() onOkPressed});
Future<void> handleError({required Failure failure, Function() onOkPressed, Function(Failure)? onUnHandledFailure});
}
class ErrorHandlerServiceImp implements ErrorHandlerService {
@ -23,7 +24,7 @@ class ErrorHandlerServiceImp implements ErrorHandlerService {
});
@override
Future<void> handleError({required Failure failure, Function()? onOkPressed}) async {
Future<void> 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);
}
}

@ -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,

@ -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<GenericBottomSheet> {
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<GenericBottomSheet> {
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<GenericBottomSheet> {
: 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<GenericBottomSheet> {
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) {

@ -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();

@ -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<CustomCountryDropdown> {
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
return Container(
height: 40.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 10.h),
@ -120,7 +123,11 @@ class _CustomCountryDropdownState extends State<CustomCountryDropdown> {
),
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<CustomCountryDropdown> {
if (textFocusNode.hasFocus) {
textFocusNode.unfocus();
}
AppState appState = getIt.get<AppState>();
RenderBox renderBox = context.findRenderObject() as RenderBox;
Offset offset = renderBox.localToGlobal(Offset.zero);
@ -172,7 +179,9 @@ class _CustomCountryDropdownState extends State<CustomCountryDropdown> {
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)),
],
),
),

@ -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<AppState>();
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,
),
);
}

@ -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<double> 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<StatefulWidget> createState() {
return OTPWidgetState();
}
}
class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixin {
late AnimationController _highlightAnimationController;
late FocusNode focusNode;
String text = "";
int currentIndex = 0;
List<String> 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: <Widget>[
_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 ? <TextInputFormatter>[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<Widget> 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<String>("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<double> animation) {
return child;
},
child: Text(
text,
key: ValueKey<String>("$text$i"),
style: widget.pinTextStyle,
),
);
} else {
return Text(
text,
key: ValueKey<String>("${strList[i]}$i"),
style: widget.pinTextStyle,
);
}
}
}
Loading…
Cancel
Save