import 'dart:async'; import 'dart:io'; import 'package:easy_localization/src/public_ext.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:mohem_flutter_app/api/login_api_client.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:mohem_flutter_app/widgets/otp_widget.dart'; import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; final ValueNotifier otpFieldClear = ValueNotifier(""); class OtpDialog { final int type; final int? mobileNo; final Function(String, TextEditingController _pinPutController) onSuccess; final Function onFailure; final BuildContext context; final Function onResendCode; int remainingTime = 120; Future? timer; OtpDialog(this.context, this.type, this.mobileNo, this.onSuccess, this.onFailure, {required this.onResendCode}); GlobalKey? verifyAccountForm = GlobalKey(); final TextEditingController _pinPutController = TextEditingController(); TextEditingController digit1 = TextEditingController(text: ""); TextEditingController digit2 = TextEditingController(text: ""); TextEditingController digit3 = TextEditingController(text: ""); TextEditingController digit4 = TextEditingController(text: ""); Map verifyAccountFormValue = {'digit1': '', 'digit2': '', 'digit3': '', 'digit4': ''}; final focusD1 = FocusNode(); final focusD2 = FocusNode(); final focusD3 = FocusNode(); final focusD4 = FocusNode(); String? errorMsg; // ProjectViewModel projectProvider; String displayTime = ''; dynamic setState; bool stopTimer = false; bool isCodeExpired = false; // static String signature; void displayDialog(BuildContext context) async { otpFieldClear.value = ""; _startListeningForSms(); showDialog( context: context, barrierColor: Colors.black.withOpacity(0.63), builder: (context) { // projectProvider = Provider.of(context); return Dialog( backgroundColor: Colors.white, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), insetPadding: const EdgeInsets.only(left: 21, right: 21), child: StatefulBuilder( builder: (context, setState) { if (displayTime == '') { startTimer(setState); } return Container( padding: EdgeInsets.all(21), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ SvgPicture.asset( type == 1 ? "assets/images/login/verify_sms.svg" : type == 2 ? "assets/images/login/verify_whatsapp.svg" : type == 3 ? "assets/images/login/verify_face.svg" : 'assets/images/login/verify_thumb.svg', height: 50, width: 50, ), 12.height, LocaleKeys.otp.tr().toText14(), LocaleKeys.verification.tr().toText24(isBold: true), 6.height, Builder( builder: (context) { String maskedNumber = ""; if (mobileNo != null) { String numStr = mobileNo.toString(); if (numStr.length >= 3) { maskedNumber = ' xxxxxxxx' + numStr.substring(numStr.length - 3); } else { maskedNumber = ' ' + numStr; } } return (LocaleKeys.pleaseEnterTheVerificationCodeSentTo.tr() + maskedNumber).toText16(); }, ), 18.height, ValueListenableBuilder( builder: (BuildContext context, String value, Widget? child) { // This builder will only get called when the _counter // is updated. return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Directionality( textDirection: TextDirection.ltr, child: Center( child: OTPWidget( autoFocus: true, controller: _pinPutController, defaultBorderColor: const Color(0xffD8D8D8), maxLength: 4, onTextChanged: (text) {}, pinBoxColor: Colors.white, onDone: (code) => _onOtpCallBack(code, null), textBorderColor: const Color(0xffD8D8D8), pinBoxWidth: 60, pinBoxHeight: 60, pinTextStyle: const TextStyle(fontSize: 24.0, color: MyColors.darkTextColor), pinTextAnimatedSwitcherTransition: ProvidedPinBoxTextAnimation.scalingTransition, pinTextAnimatedSwitcherDuration: const Duration(milliseconds: 300), pinBoxRadius: 10, keyboardType: TextInputType.number, ), ), ), 10.height, isCodeExpired ? Row( children: [ Expanded(child: LocaleKeys.codeExpire.tr().toText16(color: MyColors.redColor)), 12.width, Image.asset("assets/icons/ic_alarm.png", width: 20, height: 20, color: MyColors.redColor), ], ) : RichText( text: TextSpan( text: LocaleKeys.theVerificationCodeWillExpireIn.tr() + '\n', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: -0.48), children: [ TextSpan(text: displayTime, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.textMixColor, letterSpacing: -0.48)), ], ), ), ], ); }, valueListenable: otpFieldClear, ), 18.height, DefaultButton(isCodeExpired ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), () async { if (isCodeExpired) { hideSMSBox(context); onResendCode(); } else { stopTimer = true; hideSMSBox(context); } }, colors: isCodeExpired ? null : [MyColors.pinkDarkColor, MyColors.pinkDarkColor]), ], ), ); }, ), ); }, ).then((_) { stopTimer = true; _stopListeningForSms(); }); } InputDecoration buildInputDecoration(BuildContext context) { return InputDecoration( counterText: " ", enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(10)), borderSide: BorderSide(color: Colors.black)), focusedBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Theme.of(context).primaryColor)), errorBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Colors.red)), // focusedErrorBorder: OutlineInputBorder( // borderRadius: const BorderRadius.all(Radius.circular(10.0)), // borderSide: BorderSide(color: Colors.red), // // borderSide: BorderSide(color: Theme.of(context).colorScheme.error), // ), focusedErrorBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Theme.of(context).colorScheme.error)), ); } // String validateCodeDigit(value) { // if (value.isEmpty) { // return ' '; // } else if (value.length == 3) { // print(value); // } else { // return null; // } // } String getSecondsAsDigitalClock(int inputSeconds) { int secNum = int.parse(inputSeconds.toString()); // don't forget the second param int hours = (secNum / 3600).floor(); int minutes = ((secNum - hours * 3600) / 60).floor(); double seconds = secNum - hours * 3600 - minutes * 60; String minutesString = ""; String secondsString = ""; minutesString = minutes < 10 ? "0" + minutes.toString() : minutes.toString(); secondsString = seconds < 10 ? "0" + seconds.toStringAsFixed(0) : seconds.toStringAsFixed(0); return minutesString + ":" + secondsString; } void startTimer(setState) { if (stopTimer) return; remainingTime--; if (stopTimer) return; try { setState(() { displayTime = getSecondsAsDigitalClock(remainingTime); }); } catch (_) {} timer = Future.delayed(const Duration(seconds: 1), () { if (stopTimer) return; if (remainingTime > 0) { startTimer(setState); } else { if (stopTimer) return; try { setState(() { isCodeExpired = true; stopTimer = true; }); } catch (_) {} _stopListeningForSms(); } }); } void hideSMSBox(context) { stopTimer = true; _stopListeningForSms(); onFailure(); } void _onOtpCallBack(String otpCode, bool? isAutofill) { if (isCodeExpired) return; if (otpCode.length == 4) { _stopListeningForSms(); otpFieldClear.value = otpCode; onSuccess(otpCode, _pinPutController); } } void _startListeningForSms() { if (!Platform.isAndroid) return; try { SmsVerification.startListeningSms() .then((String? message) { if (stopTimer || isCodeExpired) return; if (message != null && message.isNotEmpty) { debugPrint("SMS OTP Auto-Verify received message: $message"); String? otpCode = extractOtpCode(message); if (otpCode != null && otpCode.length == 4) { debugPrint("Auto-filling OTP: $otpCode"); _pinPutController.text = otpCode; _pinPutController.selection = TextSelection.collapsed(offset: otpCode.length); _onOtpCallBack(otpCode, true); } } }) .catchError((e) { debugPrint("Error in startListeningSms: $e"); }); } catch (e) { debugPrint("Error starting SMS listening: $e"); } } void _stopListeningForSms() { if (!Platform.isAndroid) return; try { SmsVerification.stopListening(); } catch (e) { debugPrint("Error stopping SMS listening: $e"); } } static String? extractOtpCode(String? message) { if (message == null || message.trim().isEmpty) return null; String normalized = _normalizeDigits(message); // 1. Try matching with OTP context keyword if available RegExp contextRegex = RegExp(r'(?:code|otp|رمز|كود|تفعيل|تحقق)[^\d]{0,15}(\d{4})\b', caseSensitive: false); Match? contextMatch = contextRegex.firstMatch(normalized); if (contextMatch != null && contextMatch.group(1) != null) { return contextMatch.group(1); } // 2. Try exact 4-digit word boundary match Iterable matches = RegExp(r'\b\d{4}\b').allMatches(normalized); if (matches.isNotEmpty) { return matches.first.group(0); } // 3. Fallback try { Iterable allDigits = RegExp(r'\d{4}').allMatches(normalized); if (allDigits.isNotEmpty) { return allDigits.first.group(0); } } catch (_) {} return null; } static String _normalizeDigits(String input) { const String arabic = '٠١٢٣٤٥٦٧٨٩'; const String english = '0123456789'; String res = input; for (int i = 0; i < arabic.length; i++) { res = res.replaceAll(arabic[i], english[i]); } return res; } static Future getSignature() async { return await LoginApiClient.getSignature(); } }