396 lines
14 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|