TPIN Set Screen OTP Implemented

This commit is contained in:
2025-09-11 16:18:37 +05:30
parent 191610c9b2
commit 0f205873a9
3 changed files with 241 additions and 147 deletions

View File

@@ -22,6 +22,23 @@ class ChangePasswordService {
return response.toString(); return response.toString();
} }
Future getOtpTpin({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {
'mobileNumber': mobileNumber,
'type': "CHANGE_TPIN"
},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
}
print(response.toString());
return response.toString();
}
Future validateOtp({ Future validateOtp({
required String otp, required String otp,

View File

@@ -1,6 +1,8 @@
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart';
import 'package:kmobile/api/services/change_password_service.dart';
import 'package:kmobile/di/injection.dart';
class TpinOtpScreen extends StatefulWidget { class TpinOtpScreen extends StatefulWidget {
const TpinOtpScreen({super.key}); const TpinOtpScreen({super.key});
@@ -10,11 +12,13 @@ class TpinOtpScreen extends StatefulWidget {
} }
class _TpinOtpScreenState extends State<TpinOtpScreen> { class _TpinOtpScreenState extends State<TpinOtpScreen> {
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode()); final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
final List<TextEditingController> _controllers = List.generate( final List<TextEditingController> _controllers = List.generate(
4, 6,
(_) => TextEditingController(), (_) => TextEditingController(),
); );
bool _isLoading = false;
final ChangePasswordService _changePasswordService = getIt<ChangePasswordService>();
@override @override
void dispose() { void dispose() {
@@ -28,7 +32,7 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
} }
void _onOtpChanged(int idx, String value) { void _onOtpChanged(int idx, String value) {
if (value.length == 1 && idx < 3) { if (value.length == 1 && idx <5) {
_focusNodes[idx + 1].requestFocus(); _focusNodes[idx + 1].requestFocus();
} }
if (value.isEmpty && idx > 0) { if (value.isEmpty && idx > 0) {
@@ -39,18 +43,38 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
String get _enteredOtp => _controllers.map((c) => c.text).join(); String get _enteredOtp => _controllers.map((c) => c.text).join();
void _verifyOtp() { void _verifyOtp() async {
if (_enteredOtp == '0000') { setState(() {
_isLoading = true;
});
try {
// IMPORTANT: You may need to pass the mobile number here as well
await _changePasswordService.validateOtp(
otp: _enteredOtp,
mobileNumber: '8981274001', // Replace with actual mobile number
);
if (mounted) {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (_) => TpinSetScreen()), MaterialPageRoute(builder: (_) => const TpinSetScreen()),
); );
} else { }
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)), SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
); );
} }
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
} }
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -64,7 +88,6 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
body: Center( body: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
// mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(
Icons.lock_outline, Icons.lock_outline,
@@ -90,9 +113,9 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
const SizedBox(height: 32), const SizedBox(height: 32),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (i) { children: List.generate(6, (i) {
return Container( return Container(
width: 48, width: 32,
margin: const EdgeInsets.symmetric(horizontal: 8), margin: const EdgeInsets.symmetric(horizontal: 8),
child: TextField( child: TextField(
controller: _controllers[i], controller: _controllers[i],
@@ -101,7 +124,7 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLength: 1, maxLength: 1,
obscureText: true, obscureText: true,
obscuringCharacter: '', obscuringCharacter: '*',
decoration: InputDecoration( decoration: InputDecoration(
counterText: '', counterText: '',
filled: true, filled: true,
@@ -128,7 +151,17 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.verified_user_rounded), // Update icon to show a loading indicator
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.verified_user_rounded),
label: Text( label: Text(
AppLocalizations.of(context).verifyOtp, AppLocalizations.of(context).verifyOtp,
style: const TextStyle( style: const TextStyle(
@@ -144,12 +177,15 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
), ),
), ),
onPressed: _enteredOtp.length == 4 ? _verifyOtp : null, // Update onPressed to handle loading state
onPressed: (_enteredOtp.length == 6 && !_isLoading)
? _verifyOtp
: null,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton( TextButton(
onPressed: () { onPressed: () {
// Resend OTP logic here // You can also add a getOtp call here for resending
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(AppLocalizations.of(context).otpResent), content: Text(AppLocalizations.of(context).otpResent),
@@ -165,4 +201,4 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
), ),
); );
} }
} }

View File

@@ -1,10 +1,48 @@
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/fund_transfer/screens/tpin_otp_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/tpin_otp_screen.dart';
import 'package:kmobile/api/services/change_password_service.dart'; // <-- Add this import
import 'package:kmobile/di/injection.dart'; // <-- Add this import
class TpinSetupPromptScreen extends StatelessWidget { class TpinSetupPromptScreen extends StatefulWidget {
const TpinSetupPromptScreen({super.key}); const TpinSetupPromptScreen({super.key});
@override
State<TpinSetupPromptScreen> createState() => _TpinSetupPromptScreenState();
}
class _TpinSetupPromptScreenState extends State<TpinSetupPromptScreen> {
// 3. Add state variables
bool _isLoading = false;
final ChangePasswordService _changePasswordService = getIt<ChangePasswordService>();
Future<void> _getOtp() async {
setState(() {
_isLoading = true;
});
try {
await _changePasswordService.getOtp(mobileNumber: '8981274001');
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const TpinOtpScreen()),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${e.toString()}')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -37,12 +75,20 @@ class TpinSetupPromptScreen extends StatelessWidget {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.arrow_forward_rounded), icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.arrow_forward_rounded),
label: Text( label: Text(
AppLocalizations.of(context).setTpin, AppLocalizations.of(context).setTpin,
style: style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.onPrimary, backgroundColor: theme.colorScheme.onPrimary,
@@ -54,13 +100,8 @@ class TpinSetupPromptScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
), ),
onPressed: () { onPressed: _isLoading ? null : _getOtp, // <-- Use the new function
Navigator.pushReplacement( ),
context,
MaterialPageRoute(builder: (_) => const TpinOtpScreen()),
);
},
),
const SizedBox(height: 18), const SizedBox(height: 18),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0), padding: const EdgeInsets.symmetric(horizontal: 18.0),