pull/8/head
aamir-csol 2 months ago
parent 8f1e4500cc
commit 67e580447e

@ -787,7 +787,8 @@
"iAcceptTermsConditions": "أوافق على الشروط والأحكام",
"alreadyHaveAccount": "هل لديك حساب بالفعل؟",
"loginNow": "تسجيل الدخول الآن",
"notice": "إشعار"
"notice": "إشعار",
"oR": "أو",
"sendOTPWHATSAPP": "أرسل لي OTP عبر واتساب",
"sendOTPSMS": "أرسل لي OTP عبر الرسائل القصيرة"
}

@ -783,5 +783,8 @@
"iAcceptTermsConditions": "I Accept the Terms and Conditions",
"alreadyHaveAccount": "Already have an account?",
"loginNow": "Login Now",
"notice": "Notice"
"notice": "Notice",
"oR": "OR",
"sendOTPWHATSAPP": "Send me OTP on Whatsapp",
"sendOTPSMS": "Send me OTP on SMS"
}

@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
extension ContextUtils on BuildContext {
double get screenHeight => MediaQuery.of(this).size.height;
double get screenWidth => MediaQuery.of(this).size.width;
ThemeData get theme => Theme.of(this);
TextTheme get textTheme => theme.textTheme;
// TextStyle get headline1 => textTheme.headline1!;
// TextStyle get headline2 => textTheme.headline2!;
@ -14,3 +17,17 @@ extension ContextUtils on BuildContext {
// TextStyle get bodyText1 => textTheme.bodyText1!;
// TextStyle get bodyText2 => textTheme.bodyText2!;
}
extension ShowBottomSheet on BuildContext {
Future<T?> showBottomSheet<T>({isScrollControlled = true, isDismissible = false, required Widget child, Color? backgroundColor, enableDra = false, useSafeArea = false}) {
return showModalBottomSheet<T>(
context: this,
isScrollControlled: isScrollControlled,
isDismissible: isDismissible,
enableDrag: enableDra,
useSafeArea: useSafeArea,
backgroundColor: backgroundColor ?? Colors.transparent,
builder: (_) => child,
);
}
}

@ -786,5 +786,8 @@ abstract class LocaleKeys {
static const alreadyHaveAccount = 'alreadyHaveAccount';
static const loginNow = 'loginNow';
static const notice = 'notice';
static const oR = 'oR';
static const sendOTPWHATSAPP = 'sendOTPWHATSAPP';
static const sendOTPSMS = 'sendOTPSMS';
}

@ -4,12 +4,14 @@ 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/core/utils/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';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/authentication/register.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart';
import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
@ -77,7 +79,22 @@ class _LoginScreen extends State<LoginScreen> {
text: LocaleKeys.login.tr(),
icon: AppAssets.login1,
iconColor: Colors.white,
onPressed: () {},
onPressed: () {
showLoginModel(context: context);
// if (nationIdController.text.isNotEmpty) {
// } else {
// showBottomSheet(
// child: ExceptionBottomSheet(
// message: TranslationBase.of(context).pleaseEnterNationalIdOrFileNo,
// showCancel: false,
// onOkPressed: () {
// Navigator.of(context).pop();
// },
// ),
// );
// }
},
),
SizedBox(height: 10.h), // Adjusted to sizer unit (approx 14px)
Center(
@ -119,4 +136,61 @@ class _LoginScreen extends State<LoginScreen> {
),
);
}
void showLoginModel({required BuildContext context, TextEditingController? textController}) {
context.showBottomSheet(
isScrollControlled: true,
isDismissible: false,
useSafeArea: true,
backgroundColor: Colors.transparent,
child: StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) {
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: SingleChildScrollView(
child: GenericBottomSheet(
countryCode: "966",
initialPhoneNumber: "",
textController: TextEditingController(),
isEnableCountryDropdown: true,
onCountryChange: (value) {},
onChange: (String? value) {},
buttons: [
Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(),
onPressed: () {},
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: () {},
backgroundColor: Colors.white,
borderColor: AppColors.borderOnlyColor,
textColor: AppColors.textColor,
icon: AppAssets.whatsapp,
),
),
],
),
),
);
}));
}
}

@ -12,10 +12,12 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart';
import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart' show CustomButton;
import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart';
import 'package:hmg_patient_app_new/widgets/dropdown/dropdown_widget.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
import 'package:hmg_patient_app_new/widgets/otp/otp.dart';
class RegisterNew extends StatefulWidget {
@override
@ -66,7 +68,12 @@ class _RegisterNew extends State<RegisterNew> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.showLottie(context: context, assetPath: 'assets/animations/lottie/register.json', width: 200.h, height: 200.h, fit: BoxFit.cover, repeat: true),
Utils.showLottie(context: context,
assetPath: 'assets/animations/lottie/register.json',
width: 200.h,
height: 200.h,
fit: BoxFit.cover,
repeat: true),
SizedBox(height: 16.h),
LocaleKeys.prepareToElevate.tr().toText32(isBold: true),
SizedBox(height: 24.h),
@ -82,22 +89,6 @@ class _RegisterNew extends State<RegisterNew> {
onCountryChange: (Country? value) {},
isRtl: Directionality.of(context) == TextDirection.LTR,
).withVerticalPadding(8.h),
// DropdownWidget(
// labelText: LocaleKeys.country.tr(),
// hintText: LocaleKeys.ksa.tr(),
// isEnable: true,
// selectedValue: appState.getLanguageID(context) == "1" ? "selectedCountry.nameArabic" : "selectedCountry.displayName",
// dropdownItems: Country.values.map((e) => appState.getLanguageID(context) == " 1" ? e.displayName : e.displayName).toList(),
// onChange: (val) {
// if (val != null) {}
// },
// isBorderAllowed: false,
// hasSelectionCustomIcon: true,
// isAllowRadius: false,
// padding: EdgeInsets.symmetric(vertical: 8.h),
// selectionCustomIcon: AppAssets.arrow_down,
// ).withVerticalPadding(8),
Divider(height: 1.h),
TextInputWidget(
labelText: LocaleKeys.nationalIdNumber.tr(),
@ -108,7 +99,6 @@ class _RegisterNew extends State<RegisterNew> {
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
autoFocus: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.student_card,
@ -166,7 +156,9 @@ class _RegisterNew extends State<RegisterNew> {
CustomButton(
text: "Register",
icon: AppAssets.note_edit,
onPressed: () {},
onPressed: () {
showRegisterModel(context: context);
},
),
SizedBox(height: 14),
Center(
@ -203,9 +195,121 @@ class _RegisterNew extends State<RegisterNew> {
],
),
),
),)
,
)
),
),
));
}
void showRegisterModel({required BuildContext context, TextEditingController? textController}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: false,
useSafeArea: true,
backgroundColor: Colors.transparent,
builder: (bottomSheetContext) =>
Padding(
padding: EdgeInsets.only(bottom: MediaQuery
.of(bottomSheetContext)
.viewInsets
.bottom),
child: SingleChildScrollView(
child: GenericBottomSheet(
countryCode: "966",
initialPhoneNumber: "",
textController: TextEditingController(),
onChange: (String? value) {},
buttons: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: CustomButton(
text: LocaleKeys.sendOTPSMS.tr(),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context) => OTPVerificationPage(phoneNumber: '12234567',)));
// if (mobileNo.isEmpty) {
// context.showBottomSheet(
// child: ExceptionBottomSheet(
// message: TranslationBase.of(context).pleaseEnterMobile,
// showCancel: false,
// onOkPressed: () {
// Navigator.of(context).pop();
// },
// ),
// );
// } else if (!Utils.validateMobileNumber(mobileNo)) {
// context.showBottomSheet(
// child: ExceptionBottomSheet(
// message: TranslationBase.of(context).pleaseEnterValidMobile,
// showCancel: false,
// onOkPressed: () {
// Navigator.of(context).pop();
// },
// ),
// );
// } else {
// registerUser(1);
// }
},
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: () {
// if (mobileNo.isEmpty) {
// context.showBottomSheet(
// child: ExceptionBottomSheet(
// message: TranslationBase.of(context).pleaseEnterMobile,
// showCancel: false,
// onOkPressed: () {
// Navigator.of(context).pop();
// },
// ),
// );
// } else if (!Utils.validateMobileNumber(mobileNo)) {
// context.showBottomSheet(
// child: ExceptionBottomSheet(
// message: TranslationBase.of(context).pleaseEnterValidMobile,
// showCancel: false,
// onOkPressed: () {
// Navigator.of(context).pop();
// },
// ),
// );
// } else {
// registerUser(4);
// }
// int? val = Utils.onOtpBtnPressed(OTPType.whatsapp, mobileNo, context);
// registerUser(val);
},
backgroundColor: AppColors.whiteColor,
borderColor: AppColors.borderOnlyColor,
textColor: AppColors.textColor,
icon: AppAssets.whatsapp,
),
),
],
),
),
),
);
}
}

@ -29,6 +29,7 @@ class AppColors {
static const Color bgGreenColor = Color(0xFF18C273);
static const Color textColor = Color(0xFF2E3039);
static const Color borderOnlyColor = Color(0xFF2E3039);
static const Color blackBgColor = Color(0xFF2E3039);
static const blackColor = textColor;
static const inputLabelTextColor = Color(0xff898A8D);

@ -11,11 +11,13 @@ import '../../generated/locale_keys.g.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final VoidCallback onBackPressed;
final ValueChanged<String> onLanguageChanged;
bool hideLogoAndLang;
const CustomAppBar({
CustomAppBar({
Key? key,
required this.onBackPressed,
required this.onLanguageChanged,
this.hideLogoAndLang = false,
}) : super(key: key);
@override
@ -43,11 +45,12 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
),
// Logo
if (!hideLogoAndLang)
Utils.buildSvgWithAssets(
icon: AppAssets.habiblogo,
),
// Language Selector
if (!hideLogoAndLang)
Expanded(
child: Align(
alignment: Alignment.centerRight,

@ -3,9 +3,12 @@ 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/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/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
class GenericBottomSheet extends StatefulWidget {
@ -18,10 +21,11 @@ class GenericBottomSheet extends StatefulWidget {
final bool isEnableCountryDropdown;
final bool isFromSavedLogin;
Function(String?)? onChange;
// FocusNode myFocusNode;
GenericBottomSheet(
{this.countryCode = "",
GenericBottomSheet({
this.countryCode = "",
this.initialPhoneNumber = "",
required this.buttons,
this.textController,
@ -64,11 +68,8 @@ class _GenericBottomSheetState extends State<GenericBottomSheet> {
child: Directionality(
textDirection: Directionality.of(context),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Color(0xFFF8F8FA),
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
padding: EdgeInsets.all(24.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.bgScaffoldColor, borderRadius: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
@ -90,14 +91,13 @@ class _GenericBottomSheetState extends State<GenericBottomSheet> {
Navigator.of(context).pop();
},
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: Utils.buildSvgWithAssets(
icon: AppAssets.cross_circle,
padding: EdgeInsets.only(top: 10.h),
child: Utils.buildSvgWithAssets(icon: AppAssets.cross_circle),
),
),
)),
],
),
const SizedBox(height: 8),
SizedBox(height: 8.h),
// Subtitle
widget.isFromSavedLogin
? LocaleKeys.pleaseChooseOption.tr().toText16()
@ -113,7 +113,7 @@ class _GenericBottomSheetState extends State<GenericBottomSheet> {
labelText: widget.isForEmail ? LocaleKeys.email : LocaleKeys.phoneNumber,
hintText: widget.isForEmail ? "demo@gmail.com" : "5xxxxxxxx",
controller: widget.textController!,
padding: EdgeInsets.only(top: 8, bottom: 8, left: 8, right: 8),
padding: EdgeInsets.all(8.h),
keyboardType: widget.isForEmail ? TextInputType.emailAddress : TextInputType.number,
onChange: (value) {
widget.textController!.text = value!;
@ -133,7 +133,7 @@ class _GenericBottomSheetState extends State<GenericBottomSheet> {
: SizedBox(),
],
SizedBox(height: 24),
SizedBox(height: 24.h),
...widget.buttons,
],
),

@ -76,17 +76,7 @@ class CustomButton extends StatelessWidget {
],
),
),
)
// .toSmoothContainer(
// smoothness: 1,
// side: BorderSide(width: borderWidth, color: backgroundColor),
// borderRadius: BorderRadius.circular(borderRadius * 1.2),
// foregroundDecoration: BoxDecoration(
// color: isDisabled ? backgroundColor.withOpacity(0.5) : Colors.transparent,
// borderRadius: BorderRadius.circular(borderRadius),
// ),
// ),
),
);
}
}

@ -0,0 +1,227 @@
import 'dart:async';
import 'package:flutter/material.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/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart';
class OTPVerificationPage extends StatefulWidget {
final String phoneNumber;
const OTPVerificationPage({Key? key, required this.phoneNumber}) : super(key: key);
@override
State<OTPVerificationPage> createState() => _OTPVerificationPageState();
}
class _OTPVerificationPageState extends State<OTPVerificationPage> {
final int _otpLength = 4;
late final List<TextEditingController> _controllers;
late final List<FocusNode> _focusNodes;
Timer? _resendTimer;
int _resendTime = 60;
bool _isOtpComplete = false;
@override
void initState() {
super.initState();
_controllers = List.generate(_otpLength, (_) => TextEditingController());
_focusNodes = List.generate(_otpLength, (_) => FocusNode());
_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();
_resendTimer?.cancel();
super.dispose();
}
void _startResendTimer() {
_resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_resendTime > 0) {
setState(() => _resendTime--);
} else {
timer.cancel();
}
});
}
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();
}
_checkOtpCompletion();
}
void _checkOtpCompletion() {
final isComplete = _controllers.every((c) => c.text.isNotEmpty);
if (isComplete != _isOtpComplete) {
setState(() => _isOtpComplete = isComplete);
if (isComplete) {
_verifyOtp();
}
}
}
void _resendOtp() {
if (_resendTime == 0) {
setState(() => _resendTime = 60);
_startResendTimer();
autoFillOtp("1234");
// call resend API here
}
}
String _getMaskedPhoneNumber() {
final phone = widget.phoneNumber;
return phone.length > 4 ? '05xxxxxx${phone.substring(phone.length - 2)}' : phone;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
hideLogoAndLang: true,
onBackPressed: () {
Navigator.of(context).pop();
},
onLanguageChanged: (lang) {},
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 40.h),
Text(
'OTP Verification',
style: TextStyle(fontSize: 24.fSize, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
Text(
'We have sent you the OTP code on ${_getMaskedPhoneNumber()} via SMS for registration verification',
style: TextStyle(fontSize: 16.fSize, color: Colors.grey),
),
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),
),
),
);
},
);
}),
),
),
const SizedBox(height: 32),
// Resend OTP
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Didn't receive it? "),
if (_resendTime > 0)
Text(
'resend in (${_resendTime.toString().padLeft(2, '0')}:00). ',
style: const TextStyle(color: Colors.grey),
)
else
GestureDetector(
onTap: _resendOtp,
child: const Text(
'Resend',
style: TextStyle(
color: AppColors.primaryRedColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
);
}
void _verifyOtp() {
final otp = _controllers.map((c) => c.text).join();
debugPrint('Verifying OTP: $otp');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Verifying OTP: $otp')),
);
}
/// Auto fill OTP into text fields
void autoFillOtp(String otp) {
if (otp.length != _otpLength) return;
for (int i = 0; i < _otpLength; i++) {
_controllers[i].text = otp[i];
}
// Move focus to the last field
_focusNodes[_otpLength - 1].requestFocus();
// Trigger completion check and color update
_checkOtpCompletion();
}
}
Loading…
Cancel
Save