Merge branch 'master' into haroon_dev

# Conflicts:
#	lib/core/dependencies.dart
#	lib/extensions/string_extensions.dart
#	lib/widgets/input_widget.dart
pull/34/head
haroon amjad 2 months ago
commit 73f8baa77e

@ -0,0 +1,5 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.46948 5.26367C5.46948 3.08905 7.23236 1.32617 9.40698 1.32617C11.5816 1.32617 13.3445 3.08905 13.3445 5.26367C13.3445 7.43829 11.5816 9.20117 9.40698 9.20117C7.23236 9.20117 5.46948 7.43829 5.46948 5.26367Z" fill="white"/>
<path d="M14.282 10.8887C14.6962 10.8887 15.032 11.2245 15.032 11.6387L15.032 13.5137H16.907C17.3212 13.5137 17.657 13.8495 17.657 14.2637C17.657 14.6779 17.3212 15.0137 16.907 15.0137H15.032V16.8887C15.032 17.3029 14.6962 17.6387 14.282 17.6387C13.8678 17.6387 13.532 17.3029 13.532 16.8887V15.0137L11.657 15.0137C11.2428 15.0137 10.907 14.6779 10.907 14.2637C10.907 13.8495 11.2428 13.5137 11.657 13.5137L13.532 13.5137L13.532 11.6387C13.532 11.2245 13.8678 10.8887 14.282 10.8887Z" fill="white"/>
<path d="M12.4228 11.3881C12.4366 11.2848 12.389 11.1802 12.2947 11.1358C11.5181 10.7701 10.6961 10.5285 9.85994 10.4106C9.06204 10.298 8.25072 10.298 7.45282 10.4106C6.35447 10.5655 5.28042 10.9338 4.30178 11.5166C4.21459 11.5685 4.10681 11.6293 3.98488 11.6981C3.45027 11.9997 2.64205 12.4557 2.08842 12.9975C1.74217 13.3365 1.41318 13.7831 1.35337 14.3302C1.28977 14.9121 1.54361 15.4582 2.05288 15.9434C2.93146 16.7804 3.9858 17.4512 5.34953 17.4512L11.9632 17.4512C12.05 17.4512 12.1356 17.4485 12.2199 17.4431C12.3579 17.4345 12.4458 17.2977 12.4259 17.1609C12.4129 17.072 12.4063 16.9811 12.4063 16.8887V16.3637C12.4063 16.2394 12.3055 16.1387 12.1812 16.1387H11.6563C10.6207 16.1387 9.78125 15.2992 9.78125 14.2637C9.78125 13.2281 10.6207 12.3887 11.6563 12.3887H12.1812C12.3055 12.3887 12.4063 12.2879 12.4063 12.1637V11.6387C12.4063 11.5537 12.4119 11.4701 12.4228 11.3881Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2875 11.1375C11.0186 11.514 10.25 12.681 10.25 13.9011L10.25 23.1109V23.1109C10.25 24.0675 10.25 24.8561 10.3336 25.5045C10.4222 26.1908 10.6103 26.7808 11.023 27.3226C11.7991 28.3414 13.0649 28.6825 14.5937 29.0945C17.3925 29.8507 20.3234 30.2985 22.9379 30.5539C24.2263 30.6799 25.342 30.7889 26.1943 30.7385C27.0973 30.6852 27.864 30.4566 28.5222 29.8551C29.1878 29.2469 29.4818 28.4721 29.619 27.5411C29.7501 26.6519 29.75 25.523 29.75 24.1294V22.005C29.75 20.6461 29.75 19.5558 29.6434 18.6993C29.5334 17.8161 29.2987 17.0681 28.7429 16.4687C28.3097 16.0014 27.751 15.7218 27.1061 15.5394C26.8205 15.4586 26.6777 15.4182 26.6163 15.3223C26.5549 15.2265 26.5772 15.0844 26.6217 14.8C26.6965 14.3217 26.7469 13.8046 26.7499 13.287C26.7554 12.3019 26.5913 11.1247 25.887 10.2663C24.9266 9.09578 23.4925 9.22142 22.6683 9.29363C17.7941 9.70754 14.2397 10.5581 12.2875 11.1375ZM22.7412 11.1692C23.2217 11.1284 23.558 11.1064 23.8415 11.1514C24.0784 11.1889 24.2313 11.2663 24.3657 11.4302C24.6498 11.7764 24.8139 12.4144 24.809 13.2768C24.8064 13.7428 24.7549 14.2195 24.6803 14.6563C24.6397 14.8938 24.6194 15.0125 24.5281 15.084C24.4367 15.1555 24.3108 15.1461 24.0591 15.1274C23.7397 15.1037 23.4049 15.0805 23.0559 15.0563C18.8417 14.7643 15.2273 14.2127 12.8773 13.6991C12.5868 13.6356 12.4415 13.6039 12.404 13.4099C12.3665 13.216 12.4521 13.1578 12.6233 13.0413C12.6967 12.9914 12.776 12.9535 12.8581 12.9291C14.6959 12.3837 18.0184 11.5703 22.7412 11.1692ZM24.5 21C25.6046 21 26.5 21.8954 26.5 23C26.5 24.1046 25.6046 25 24.5 25C23.3954 25 22.5 24.1046 22.5 23C22.5 21.8954 23.3954 21 24.5 21Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

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

@ -51,6 +51,8 @@ class AppAssets {
static const String insurance_update_icon = '$svgBasePath/insurance_update.svg';
static const String home_calendar_icon = '$svgBasePath/home_calendar_icon.svg';
static const String add_icon = '$svgBasePath/add_icon.svg';
static const String add_family = '$svgBasePath/add_family.svg';
static const String wallet = '$svgBasePath/wallet.svg';
static const String livecare_icon = '$svgBasePath/livecare_icon.svg';
static const String file_icon = '$svgBasePath/file_icon.svg';
static const String checkmark_icon = '$svgBasePath/checkmark_icon.svg';

@ -145,6 +145,7 @@ class AppDependencies {
),
);
getIt.registerLazySingleton<MedicalFileViewModel>(
() => MedicalFileViewModel(
medicalFileRepo: getIt(),

@ -251,8 +251,9 @@ class Utils {
);
}
static bool isSAUDIIDValid(String id, type) {
if (type == 1) {
static bool isSAUDIIDValid(
String id,
) {
try {
id = id.toString();
id = id.trim();
@ -287,9 +288,6 @@ class Utils {
log("errr: ${err.toString()}");
}
return false;
} else {
return true;
}
}
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);
}
}

@ -34,7 +34,8 @@ extension EmailValidator on String {
),
);
Widget toText10({Color? color, FontWeight? weight, bool isBold = false, bool isUnderLine = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow, double letterSpacing = -1}) => Text(
Widget toText10({Color? color, FontWeight? weight, bool isBold = false, bool isUnderLine = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow, double letterSpacing = -1}) =>
Text(
this,
maxLines: maxlines,
overflow: textOverflow,
@ -119,16 +120,7 @@ extension EmailValidator on String {
),
);
Widget toText13({
Color? color,
bool isUnderLine = false,
bool isBold = false,
bool isCenter = false,
int maxLine = 0,
FontWeight? weight,
double? letterSpacing = -1
}) =>
Text(
Widget toText13({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0, FontWeight? weight, double? letterSpacing = -1}) => Text(
this,
textAlign: isCenter ? TextAlign.center : null,
maxLines: (maxLine > 0) ? maxLine : null,
@ -193,10 +185,11 @@ extension EmailValidator on String {
style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 17.fSize, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal),
);
Widget toText18({Color? color, FontWeight? weight, bool isBold = false, bool isCenter = false, int? maxlines}) => Text(
Widget toText18({Color? color, FontWeight? weight, bool isBold = false, bool isCenter = false, int? maxlines, TextOverflow? textOverflow}) => Text(
maxLines: maxlines,
textAlign: isCenter ? TextAlign.center : null,
this,
overflow: textOverflow,
style: TextStyle(fontSize: 18.fSize, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4),
);
@ -205,7 +198,7 @@ extension EmailValidator on String {
style: TextStyle(fontSize: 19.fSize, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4),
);
Widget toText20({Color? color,FontWeight? weight, bool isBold = false}) => Text(
Widget toText20({Color? color, FontWeight? weight, bool isBold = false}) => Text(
this,
style: TextStyle(fontSize: 20.fSize, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4),
);

@ -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,
);
final resultEither = await _authenticationRepo.checkActivationCodeRepo(newRequest: CheckActivationCodeRegisterReq.fromJson(request), activationCode: activationCode, isRegister: false);
resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async {
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(
LoaderBottomSheet.hideLoader();
_dialogService.showCommonBottomSheetWithoutH(
message: response["ErrorMessage"],
showCancel: true,
showOKButton: true,
onOkPressed: () {
_navigationService.popUntilNamed(AppRoutes.loginScreen);
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);
if (isComplete) {
_verifyOtp();
}
// The OTPWidget will automatically call onDone when complete
// This method can be used for any additional logic on text change
}
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,
// 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,
),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: Colors.transparent,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: BorderSide.none,
),
),
onChanged: (v) => _onOtpChanged(index, v),
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,
),
);
}
}
}

@ -0,0 +1,8 @@
import 'package:flutter/foundation.dart';
class ProfileSettingsViewModel extends ChangeNotifier {
void notify(){
notifyListeners();
}
}

@ -17,6 +17,7 @@ import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_mode
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart';
import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
@ -102,6 +103,9 @@ void main() async {
errorHandlerService: getIt(),
),
),
ChangeNotifierProvider<ProfileSettingsViewModel>(
create: (_) => ProfileSettingsViewModel(),
),
ChangeNotifierProvider<MyAppointmentsViewModel>(
create: (_) => MyAppointmentsViewModel(
myAppointmentsRepo: getIt(),

@ -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)
@ -124,8 +128,7 @@ class LoginScreenState extends State<LoginScreen> {
color: AppColors.primaryRedColor,
fontSize: 14.fSize, // Adjusted to sizer unit
height: 26 / 16, // Ratio
fontWeight: FontWeight.w500,
),
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 {
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}");
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: () {
// 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 {
// 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 {
// 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()),
);

@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:flutter_swiper_view/flutter_swiper_view.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/utils/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/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/presentation/lab/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:provider/provider.dart';
class ProfileSettings extends StatefulWidget {
ProfileSettings({Key? key}) : super(key: key);
@override
_ProfileSettingsState createState() {
return _ProfileSettingsState();
}
}
class _ProfileSettingsState extends State<ProfileSettings> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
int length = 3;
final SwiperController _controller = SwiperController();
int _index = 0;
@override
Widget build(BuildContext context) {
return CollapsingListView(
title: "Profile & Settings".needTranslation,
logout: () {},
isClose: true,
child: SingleChildScrollView(
padding: EdgeInsets.only(top: 24, bottom: 24),
physics: NeverScrollableScrollPhysics(),
child: Consumer<ProfileSettingsViewModel>(
builder: (context, model, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Swiper(
itemCount: length,
layout: SwiperLayout.STACK,
loop: true,
itemWidth: MediaQuery.of(context).size.width - 42,
indicatorLayout: PageIndicatorLayout.COLOR,
axisDirection: AxisDirection.right,
controller: _controller,
itemHeight: 210 + 16,
pagination: const SwiperPagination(
alignment: Alignment.bottomCenter,
margin: EdgeInsets.only(top: 210 + 8 + 24),
builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor),
),
itemBuilder: (BuildContext context, int index) {
return FamilyCardWidget().paddingOnly(right: 16);
},
),
GridView(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 105 / 105, crossAxisSpacing: 9, mainAxisSpacing: 9),
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(24),
shrinkWrap: true,
children: [
Container(
padding: EdgeInsets.all(16),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Column(
children: [
Row(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.h, height: 40.h),
],
)
],
),
)
],
)
],
);
},
),
),
);
}
}
class FamilyCardWidget extends StatelessWidget {
FamilyCardWidget();
@override
Widget build(BuildContext context) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Column(
children: [
Column(
spacing: 8.h,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.h,
children: [
Image.asset(true ? AppAssets.male_img : AppAssets.femaleImg, width: 56.h, height: 56.h),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 0.h,
mainAxisSize: MainAxisSize.min,
children: [
"Mahmoud Shrouf Shrouf".toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1),
AppCustomChipWidget(
icon: AppAssets.file_icon,
labelText: "File no: 3423443",
iconSize: 14,
),
],
).expanded,
Icon(Icons.qr_code, size: 56)
],
).expanded,
SizedBox(
width: double.infinity,
child: Wrap(
alignment: WrapAlignment.start,
spacing: 8.h,
children: [
AppCustomChipWidget(labelText: "35 Years Old"),
AppCustomChipWidget(labelText: "Blood: A+"),
AppCustomChipWidget(
icon: AppAssets.insurance_active_icon,
labelText: "Insurance Active",
iconColor: AppColors.bgGreenColor,
iconSize: 14,
backgroundColor: AppColors.bgGreenColor.withValues(alpha: 0.15),
),
],
),
),
],
).paddingOnly(top: 16, right: 16, left: 16, bottom: 12).expanded,
Divider(
height: 1,
thickness: 1,
color: Color(0x30D2D2D2),
),
CustomButton(icon: AppAssets.add_family, text: "Add a new family member", onPressed: () {}).paddingOnly(top: 12, right: 16, left: 16, bottom: 16),
],
),
);
}
}

@ -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,7 +126,8 @@ void showCommonBottomSheetWithoutHeight(
top: false,
left: false,
right: false,
child:isCloseButtonVisible ? Container(
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(
@ -136,7 +137,7 @@ void showCommonBottomSheetWithoutHeight(
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (title.isNotEmpty) title.toText20(weight: FontWeight.w600).expanded,
title.toText20(weight: FontWeight.w600).expanded,
Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, iconColor: Color(0xff2B353E)).onPress(() {
Navigator.of(context).pop();
}),
@ -144,7 +145,8 @@ void showCommonBottomSheetWithoutHeight(
),
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)),
],
),
),

@ -77,31 +77,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,
@ -134,7 +135,7 @@ class TextInputWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabelText(),
_buildLabelText().paddingOnly(right: (appState.getLanguageCode() == "ar" ? 10 : 0)),
_buildTextField(context),
],
),
@ -183,6 +184,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(),
@ -217,10 +219,7 @@ class TextInputWidget extends StatelessWidget {
}
Widget _buildTextField(BuildContext context) {
return KeyboardActions(
config: _keyboardActionsConfig,
disableScroll: true,
child: TextField(
return TextField(
enabled: isEnable,
scrollPadding: EdgeInsets.zero,
keyboardType: keyboardType,
@ -264,7 +263,6 @@ class TextInputWidget extends StatelessWidget {
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,
);
}
}
}

@ -78,6 +78,7 @@ dependencies:
keyboard_actions: ^4.2.0
path_provider: ^2.0.8
open_filex: ^4.7.0
flutter_swiper_view: ^1.1.8
dev_dependencies:
flutter_test:

Loading…
Cancel
Save