initial commit: push everything
This commit is contained in:
395
lib/views/onboardscreens/otp_page.dart
Normal file
395
lib/views/onboardscreens/otp_page.dart
Normal file
@@ -0,0 +1,395 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user