From ef481ec87976efbe0c1f14096d80cfdcb635bcba Mon Sep 17 00:00:00 2001 From: shital Date: Tue, 11 Nov 2025 14:18:37 +0530 Subject: [PATCH] Security settings improvements --- .gitignore | 5 + android/app/build.gradle | 20 ++- lib/di/injection.dart | 6 +- lib/features/auth/screens/mpin_screen.dart | 27 +++- .../screens/tpin_otp_screen.dart | 30 ++--- lib/features/profile/change_mpin_screen.dart | 96 ++++++++++++++ lib/features/profile/profile_screen.dart | 76 ++--------- .../profile/security_settings_screen.dart | 121 ++++++++++++++++++ lib/l10n/app_en.arb | 7 + lib/l10n/app_hi.arb | 7 + 10 files changed, 294 insertions(+), 101 deletions(-) create mode 100644 lib/features/profile/change_mpin_screen.dart create mode 100644 lib/features/profile/security_settings_screen.dart diff --git a/.gitignore b/.gitignore index 578d2e2..66425f9 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,8 @@ app.*.map.json lib/l10n/app_localizations.dart lib/l10n/app_localizations_en.dart lib/l10n/app_localizations_hi.dart + +# Keystore files +android/key.properties +android/*.jks +android/*.keystore diff --git a/android/app/build.gradle b/android/app/build.gradle index b7fa505..de387d6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,6 +22,12 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + android { namespace "com.example.kmobile" compileSdk flutter.compileSdkVersion @@ -51,15 +57,21 @@ android { versionName flutterVersionName } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig signingConfigs.release minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } } } diff --git a/lib/di/injection.dart b/lib/di/injection.dart index 63dffe9..c90d749 100644 --- a/lib/di/injection.dart +++ b/lib/di/injection.dart @@ -69,9 +69,9 @@ Dio _createDioClient() { final dio = Dio( BaseOptions( baseUrl: - 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test - //'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod - //'https://kccbmbnk.net', //prod small + //'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test + //'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod + 'https://kccbmbnk.net', //prod small connectTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60), headers: { diff --git a/lib/features/auth/screens/mpin_screen.dart b/lib/features/auth/screens/mpin_screen.dart index 010dbe0..cf14f12 100644 --- a/lib/features/auth/screens/mpin_screen.dart +++ b/lib/features/auth/screens/mpin_screen.dart @@ -15,12 +15,18 @@ class MPinScreen extends StatefulWidget { final MPinMode mode; final String? initialPin; final void Function(String pin)? onCompleted; + final bool disableBiometric; + final String? customTitle; + final String? customConfirmTitle; const MPinScreen({ super.key, required this.mode, this.initialPin, this.onCompleted, + this.disableBiometric = false, + this.customTitle, + this.customConfirmTitle, }); @override @@ -77,7 +83,7 @@ class _MPinScreenState extends State with TickerProviderStateMixin { CurvedAnimation(parent: _waveController, curve: Curves.easeInOut), ); - if (widget.mode == MPinMode.enter) { + if (widget.mode == MPinMode.enter && !widget.disableBiometric) { _tryBiometricBeforePin(); } } @@ -172,17 +178,27 @@ class _MPinScreenState extends State with TickerProviderStateMixin { } break; case MPinMode.set: - // propagate parent onCompleted into confirm step - Navigator.push( + // Navigate to confirm and wait for result + final result = await Navigator.push( context, MaterialPageRoute( builder: (_) => MPinScreen( mode: MPinMode.confirm, initialPin: pin, - onCompleted: widget.onCompleted, // <-- use parent callback + onCompleted: (confirmedPin) { + // Just pop with the pin, don't call parent callback yet + Navigator.of(context).pop(confirmedPin); + }, + disableBiometric: widget.disableBiometric, + customTitle: widget.customConfirmTitle, ), ), ); + + // If confirm succeeded, call parent callback + if (result != null && mounted) { + widget.onCompleted?.call(result); + } break; case MPinMode.confirm: if (widget.initialPin == pin) { @@ -335,6 +351,9 @@ class _MPinScreenState extends State with TickerProviderStateMixin { } String getTitle() { + if (widget.customTitle != null) { + return widget.customTitle!; + } switch (widget.mode) { case MPinMode.enter: return AppLocalizations.of(context).enterMPIN; diff --git a/lib/features/fund_transfer/screens/tpin_otp_screen.dart b/lib/features/fund_transfer/screens/tpin_otp_screen.dart index aecf725..af4743a 100644 --- a/lib/features/fund_transfer/screens/tpin_otp_screen.dart +++ b/lib/features/fund_transfer/screens/tpin_otp_screen.dart @@ -51,30 +51,16 @@ class _TpinOtpScreenState extends State { }); try { - // TESTING BYPASS: Accept any 6-digit number as valid OTP - if (_enteredOtp.length == 6) { - // Skip API validation for testing - await Future.delayed(const Duration(milliseconds: 500)); // Simulate API delay + await _changePasswordService.validateOtp( + otp: _enteredOtp, + mobileNumber: widget.mobileNumber, + ); - if (mounted) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (_) => const TpinSetScreen()), - ); - } - } else { - // Regular validation - await _changePasswordService.validateOtp( - otp: _enteredOtp, - mobileNumber: widget.mobileNumber, + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const TpinSetScreen()), ); - - if (mounted) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (_) => const TpinSetScreen()), - ); - } } } catch (e) { if (mounted) { diff --git a/lib/features/profile/change_mpin_screen.dart b/lib/features/profile/change_mpin_screen.dart new file mode 100644 index 0000000..d501b9c --- /dev/null +++ b/lib/features/profile/change_mpin_screen.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:kmobile/features/auth/screens/mpin_screen.dart'; +import 'package:kmobile/security/secure_storage.dart'; +import 'package:kmobile/di/injection.dart'; +import '../../l10n/app_localizations.dart'; + +class ChangeMpinScreen extends StatefulWidget { + const ChangeMpinScreen({super.key}); + + @override + State createState() => _ChangeMpinScreenState(); +} + +class _ChangeMpinScreenState extends State { + @override + void initState() { + super.initState(); + // Start the flow after the widget is built + WidgetsBinding.instance.addPostFrameCallback((_) { + _startChangeMpin(); + }); + } + + void _startChangeMpin() async { + final loc = AppLocalizations.of(context); + + // Step 1: Verify old PIN + final oldPinVerified = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MPinScreen( + mode: MPinMode.enter, + disableBiometric: true, + customTitle: loc.enterOldMpin, + onCompleted: (oldPin) => _verifyOldPin(oldPin), + ), + ), + ); + + if (oldPinVerified != true) { + if (mounted) Navigator.of(context).pop(false); + return; + } + + // Step 2 & 3: Set new PIN (which will internally navigate to confirm) + // The onCompleted will be called after both set and confirm succeed + final success = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MPinScreen( + mode: MPinMode.set, + customTitle: loc.enterNewMpin, + customConfirmTitle: loc.confirmNewMpin, + onCompleted: (newPin) async { + // This is called after confirm succeeds and PIN is saved + if (context.mounted) { + Navigator.of(context).pop(true); + } + }, + ), + ), + ); + + if (mounted) { + Navigator.of(context).pop(success == true); + } + } + + Future _verifyOldPin(String oldPin) async { + final storage = getIt(); + final storedPin = await storage.read('mpin'); + + if (storedPin == int.tryParse(oldPin)) { + // Old PIN is correct + if (mounted) { + Navigator.of(context).pop(true); + } + } else { + // This shouldn't happen as MPinScreen handles validation + if (mounted) { + Navigator.of(context).pop(false); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).changeMpin), + centerTitle: true, + ), + body: const Center( + child: CircularProgressIndicator(), + ), + ); + } +} diff --git a/lib/features/profile/profile_screen.dart b/lib/features/profile/profile_screen.dart index f6fde2d..7fc3efc 100644 --- a/lib/features/profile/profile_screen.dart +++ b/lib/features/profile/profile_screen.dart @@ -2,10 +2,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:kmobile/data/repositories/auth_repository.dart'; -import 'package:kmobile/features/profile/change_password/change_password_screen.dart'; import 'package:kmobile/features/profile/daily_transaction_limit.dart'; import 'package:kmobile/features/profile/logout_dialog.dart'; -import 'package:kmobile/features/profile/tpin/change_tpin_screen.dart'; +import 'package:kmobile/features/profile/security_settings_screen.dart'; import 'package:kmobile/security/secure_storage.dart'; import 'package:local_auth/local_auth.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -13,8 +12,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../di/injection.dart'; import '../../l10n/app_localizations.dart'; import 'package:kmobile/features/profile/preferences/preference_screen.dart'; -import 'package:kmobile/api/services/auth_service.dart'; -import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart'; class ProfileScreen extends StatefulWidget { final String mobileNumber; @@ -201,77 +198,20 @@ class _ProfileScreenState extends State { secondary: const Icon(Icons.fingerprint), ), ListTile( - leading: const Icon(Icons.password), - title: Text(loc.changeLoginPassword), + leading: const Icon(Icons.security), + title: Text(loc.securitySettings), + trailing: const Icon(Icons.chevron_right), onTap: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => ChangePasswordScreen( - mobileNumber: widget.mobileNumber, - )), + builder: (context) => SecuritySettingsScreen( + mobileNumber: widget.mobileNumber, + ), + ), ); }, ), - ListTile( - leading: const Icon(Icons.password), - title: Text('Change TPIN'), - onTap: () async { - // 1. Get the AuthService instance - final authService = getIt(); - - // 2. Call checkTpin() to see if TPIN is set - final isTpinSet = await authService.checkTpin(); - - // 3. If TPIN is not set, show the dialog - if (!isTpinSet) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('TPIN Not Set'), - content: Text( - 'You have not set a TPIN yet. Please set a TPIN to proceed.'), - actions: [ - TextButton( - child: Text('Back'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text('Proceed'), - onPressed: () { - Navigator.of(context).pop(); // Dismiss the dialog - // Navigate to the TPIN set screen - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => TpinSetScreen(), - ), - ); - }, - ), - ], - ); - }, - ); - } else { - // Case 2: TPIN is set - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - ChangeTpinScreen(mobileNumber: widget.mobileNumber), - ), - ); - } - }, - ), - // ListTile( - // leading: const Icon(Icons.password), - // title: const Text("Change Login MPIN"), - // onTap: () async { - // }, - // ), ListTile( leading: const Icon(Icons.smartphone), title: const Text("App Version"), diff --git a/lib/features/profile/security_settings_screen.dart b/lib/features/profile/security_settings_screen.dart new file mode 100644 index 0000000..9797f07 --- /dev/null +++ b/lib/features/profile/security_settings_screen.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:kmobile/features/profile/change_password/change_password_screen.dart'; +import 'package:kmobile/features/profile/tpin/change_tpin_screen.dart'; +import 'package:kmobile/features/profile/change_mpin_screen.dart'; +import 'package:kmobile/api/services/auth_service.dart'; +import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart'; +import 'package:kmobile/di/injection.dart'; +import '../../l10n/app_localizations.dart'; + +class SecuritySettingsScreen extends StatelessWidget { + final String mobileNumber; + + const SecuritySettingsScreen({super.key, required this.mobileNumber}); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + + return Scaffold( + appBar: AppBar( + title: Text(loc.securitySettings), + centerTitle: true, + ), + body: ListView( + children: [ + ListTile( + leading: const Icon(Icons.lock_outline), + title: Text(loc.changeLoginPassword), + trailing: const Icon(Icons.chevron_right), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChangePasswordScreen( + mobileNumber: mobileNumber, + ), + ), + ); + }, + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.pin), + title: Text(loc.changeMpin), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ChangeMpinScreen(), + ), + ); + + if (result == true && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(loc.mpinChangedSuccessfully), + backgroundColor: Colors.green, + ), + ); + } + }, + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.password), + title: const Text('Change TPIN'), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final authService = getIt(); + final isTpinSet = await authService.checkTpin(); + + if (!isTpinSet) { + if (context.mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('TPIN Not Set'), + content: const Text( + 'You have not set a TPIN yet. Please set a TPIN to proceed.'), + actions: [ + TextButton( + child: const Text('Back'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('Proceed'), + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const TpinSetScreen(), + ), + ); + }, + ), + ], + ); + }, + ); + } + } else { + if (context.mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + ChangeTpinScreen(mobileNumber: mobileNumber), + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fbd9a20..a6524d9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -217,6 +217,13 @@ "enterMPIN": "Enter your mPIN", "setMPIN": "Set your mPIN", "confirmMPIN": "Confirm your mPIN", + "changeMpin": "Change mPIN", + "enterOldMpin": "Enter your old mPIN", + "enterNewMpin": "Enter your new mPIN", + "confirmNewMpin": "Confirm your new mPIN", + "mpinChangedSuccessfully": "mPIN changed successfully", + "pinMismatch": "PINs do not match", + "securitySettings": "Security Settings", "kconnect": "Kconnect", "kccBankFull": "Kangra Central Co-operative Bank", "themeColor": "Theme Color", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 975478f..a46be90 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -218,6 +218,13 @@ "enterMPIN": "अपना mPIN दर्ज करें", "setMPIN": "अपना mPIN सेट करें", "confirmMPIN": "अपना mPIN की पुष्टि करें", + "changeMpin": "mPIN बदलें", + "enterOldMpin": "अपना पुराना mPIN दर्ज करें", + "enterNewMpin": "अपना नया mPIN दर्ज करें", + "confirmNewMpin": "अपना नया mPIN की पुष्टि करें", + "mpinChangedSuccessfully": "mPIN सफलतापूर्वक बदल गया", + "pinMismatch": "PIN मेल नहीं खा रहे हैं", + "securitySettings": "सुरक्षा सेटिंग्स", "kconnect": "के-कनेक्ट", "kccBankFull": "कांगड़ा सेंट्रल को-ऑपरेटिव बैंक", "themeColor": "थीम रंग",