import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:nearle/views/helpers/constants/Colorconstants.dart'; import 'package:nearle/views/helpers/constants/Font_constant.dart'; import 'package:nearle/views/onboardscreens/Sign_in.dart'; import 'package:nearle/controllers/auth.dart'; import 'package:nearle/views/onboardscreens/Creat_mpin.dart'; import 'package:sms_autofill/sms_autofill.dart'; import 'package:shared_preferences/shared_preferences.dart'; class OtpPage extends GetResponsiveView { OtpPage({super.key}); @override Widget? phone() => _OtpPageLayout(); @override Widget? tablet() => _OtpPageLayout(scale: 1.2); @override Widget? desktop() => _OtpPageLayout(scale: 1.3); } class _OtpPageLayout extends StatefulWidget { final double scale; const _OtpPageLayout({this.scale = 1.0}); @override State<_OtpPageLayout> createState() => _OtpPageLayoutState(); } class _OtpPageLayoutState extends State<_OtpPageLayout> with CodeAutoFill { final AuthController _auth = Get.put(AuthController()); final List _otpControllers = List.generate( 6, (_) => TextEditingController(), ); final List _focusNodes = List.generate(6, (_) => FocusNode()); bool isVerifying = false; int _secondsRemaining = 60; Timer? _timer; String _appSignature = ''; // Consent fallback removed due to plugin AGP incompatibility int _smsDefaultProvider = 0; // 0 = normal, 1 = passkey provider int? _smsPassKey; // when provider is passkey based @override void initState() { super.initState(); _startTimer(); _listenForOtp(); _loadOtpProviderPrefs(); // Ensure cursor starts in first box WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && _focusNodes.isNotEmpty) { _focusNodes[0].requestFocus(); } }); } void _startTimer() { _secondsRemaining = 60; _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_secondsRemaining > 0) { setState(() => _secondsRemaining--); } else { timer.cancel(); } }); } Future _resendOtp() async { setState(() => _secondsRemaining = 60); _timer?.cancel(); _startTimer(); await _auth.sendOtp(); } Future _listenForOtp() async { try { await SmsAutoFill().unregisterListener(); listenForCode(); // Fetch and cache app hash for SMS retriever compatibility try { final sig = await SmsAutoFill().getAppSignature; if (sig.isNotEmpty) { _appSignature = sig; // Helpful for integrating with SMS provider templates debugPrint('[OTP] App signature hash: $_appSignature'); } } catch (_) {} } catch (_) {} // Consent fallback temporarily disabled; use SMS Retriever with app hash } Future _loadOtpProviderPrefs() async { try { final prefs = await SharedPreferences.getInstance(); _smsDefaultProvider = prefs.getInt('smsDefaultProvider') ?? 0; _smsPassKey = prefs.getInt('smsPassKey'); if (_smsDefaultProvider == 1 && _smsPassKey != null) { final passKeyStr = _smsPassKey!.toString().padLeft(6, '0'); // Prefill only if fields are empty if (mounted && !_otpControllers.any((c) => c.text.isNotEmpty)) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; for (int i = 0; i < 6 && i < passKeyStr.length; i++) { _otpControllers[i].text = passKeyStr[i]; } setState(() {}); }); } } } catch (_) {} } @override void codeUpdated() { final received = code ?? ''; if (received.isNotEmpty) { final digits = received.replaceAll(RegExp(r'\D'), ''); if (digits.length >= 6) { final otp = digits.substring(0, 6); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; for (int i = 0; i < 6; i++) { _otpControllers[i].text = otp[i]; } setState(() {}); // hide keyboard on autofill and verify with delay before navigation FocusScope.of(context).unfocus(); _autoVerify(delayBeforeNav: true); }); } } } Future _autoVerify({bool delayBeforeNav = false}) async { if (!mounted) return; if (!_otpControllers.every((c) => c.text.isNotEmpty)) return; if (isVerifying) return; setState(() => isVerifying = true); final entered = getOtp(); bool ok = false; // Accept provider passkey as valid OTP when enabled if (_smsDefaultProvider == 1 && _smsPassKey != null && entered == _smsPassKey!.toString().padLeft(6, '0')) { ok = true; } else { ok = await _auth.verifyOtp(entered); } setState(() => isVerifying = false); if (ok) { if (delayBeforeNav) { await Future.delayed(const Duration(seconds: 3)); } Get.to(() => CreateMpin()); } } @override void dispose() { for (final controller in _otpControllers) { controller.dispose(); } for (final node in _focusNodes) { node.dispose(); } _timer?.cancel(); try { cancel(); } catch (_) {} super.dispose(); } void _onOtpChange(String value, int index) { if (value.isNotEmpty && index < 5) { _focusNodes[index + 1].requestFocus(); } else if (value.isNotEmpty && index == 5) { FocusScope.of(context).unfocus(); } if (value.isEmpty && index > 0) { // backspace: jump focus back _focusNodes[index - 1].requestFocus(); _otpControllers[index - 1].selection = TextSelection( baseOffset: 0, extentOffset: _otpControllers[index - 1].text.length, ); } setState(() {}); } String getOtp() => _otpControllers.map((e) => e.text).join(); bool get isOtpFilled => _otpControllers.every((c) => c.text.isNotEmpty); @override Widget build(BuildContext context) { final scale = widget.scale; return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Stack( children: [ Padding( padding: EdgeInsets.symmetric( horizontal: 20 * scale, vertical: 20 * scale, ), child: SingleChildScrollView( child: Column( children: [ SizedBox(height: 60 * scale), Image.asset( 'assets/images/verify.png', fit: BoxFit.contain, height: 200 * scale, ), SizedBox(height: 24 * scale), Text( "Verify OTP", style: TextStyle( fontSize: 28 * scale, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, color: ColorConstants.primaryColor, ), ), SizedBox(height: 24 * scale), Text( "Enter the 6-digit code sent to your number", textAlign: TextAlign.center, style: TextStyle( color: Colors.black54, fontSize: 19 * scale, fontFamily: FontConstants.fontFamily, ), ), SizedBox(height: 40 * scale), // OTP Fields (6 boxes) Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(6, (index) { final isFilled = _otpControllers[index].text.isNotEmpty; return SizedBox( width: 45 * scale, height: 50 * scale, child: TextField( controller: _otpControllers[index], focusNode: _focusNodes[index], autofocus: index == 0, keyboardType: TextInputType.number, textAlign: TextAlign.center, maxLength: 1, style: TextStyle( fontSize: 17 * scale, fontWeight: FontWeight.bold, color: isFilled ? Colors.white : Colors.black, ), decoration: InputDecoration( counterText: '', filled: true, fillColor: isFilled ? ColorConstants.primaryColor : Colors.transparent, enabledBorder: OutlineInputBorder( borderSide: const BorderSide( color: Colors.grey, ), borderRadius: BorderRadius.circular(8 * scale), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: ColorConstants.primaryColor, width: 2, ), borderRadius: BorderRadius.circular(8 * scale), ), ), onChanged: (value) => _onOtpChange(value, index), ), ); }), ), SizedBox(height: 16 * scale), // Resend OTP Row( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( onPressed: _secondsRemaining == 0 ? () async { await _resendOtp(); } : null, child: Transform.translate( offset: Offset(-10, 0), child: Text( _secondsRemaining == 0 ? "Resend OTP" : "Resend in 00:${_secondsRemaining.toString().padLeft(2, '0')}", style: TextStyle( fontSize: 18 * scale, color: _secondsRemaining == 0 ? ColorConstants.primaryColor : Colors.black, fontFamily: FontConstants.fontFamily, ), ), ), ), ], ), ], ), ), ), // Back button Positioned( top: 40 * scale, left: 16 * scale, child: InkWell( onTap: () => Get.to(() => SignIn()), borderRadius: BorderRadius.circular(30 * scale), child: Container( padding: EdgeInsets.all(8 * scale), decoration: BoxDecoration( color: Colors.black12, shape: BoxShape.circle, ), child: Icon( Icons.arrow_back, color: Colors.black, size: 28 * scale, ), ), ), ), ], ), ), bottomNavigationBar: SafeArea( child: Padding( padding: EdgeInsets.all(16 * scale), child: SizedBox( height: 55 * scale, width: double.infinity, child: ElevatedButton( onPressed: isOtpFilled && !isVerifying ? () async { // Reuse common verification flow (same as auto-verify) await _autoVerify(); } : null, style: ElevatedButton.styleFrom( backgroundColor: isOtpFilled ? ColorConstants.primaryColor : Colors.grey.shade400, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10 * scale), ), ), child: isVerifying ? SizedBox( height: 30 * scale, width: 30 * scale, child: const CircularProgressIndicator( color: Colors.white, strokeWidth: 3, ), ) : Text( "Verify", style: TextStyle( fontSize: 21 * scale, fontWeight: FontWeight.bold, color: Colors.white, fontFamily: FontConstants.fontFamily, ), ), ), ), ), ), ); } }