Files

396 lines
14 KiB
Dart

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<TextEditingController> _otpControllers = List.generate(
6,
(_) => TextEditingController(),
);
final List<FocusNode> _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<void> _resendOtp() async {
setState(() => _secondsRemaining = 60);
_timer?.cancel();
_startTimer();
await _auth.sendOtp();
}
Future<void> _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<void> _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<void> _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,
),
),
),
),
),
),
);
}
}