TNC Route Fixed

This commit is contained in:
2025-11-10 12:39:31 +05:30
parent 3e88aad43f
commit 5c8df8ace3
7 changed files with 301 additions and 84 deletions

View File

@@ -42,49 +42,57 @@ class AuthCubit extends Cubit<AuthState> {
}
}
Future<void> login(String customerNo, String password) async {
emit(AuthLoading());
try {
final (users, authToken) = await _authRepository.login(customerNo, password);
Future<void> login(String customerNo, String password) async {
emit(AuthLoading());
try {
final (users, authToken) = await _authRepository.login(customerNo, password);
if (authToken.tnc == false) {
// TNC not accepted, tell UI to show the dialog
emit(ShowTncDialog(authToken, users));
} else {
// TNC already accepted, emit Authenticated and then proceed to MPIN check
emit(Authenticated(users));
await _checkMpinAndNavigate();
}
} catch (e) {
emit(AuthError(e is AuthException ? e.message : e.toString()));
}
}
if (authToken.tnc == false) {
emit(ShowTncDialog(authToken, users));
} else {
await _checkMpinAndNavigate(users);
}
} catch (e) {
emit(AuthError(e is AuthException ? e.message : e.toString()));
}
}
Future<void> onTncDialogResult(
bool agreed, AuthToken authToken, List<User> users) async {
if (agreed) {
try {
await _authRepository.acceptTnc();
// User agreed, emit Authenticated and then proceed to MPIN check
emit(Authenticated(users));
await _checkMpinAndNavigate();
} catch (e) {
emit(AuthError('Failed to accept TNC: $e'));
}
} else {
// User disagreed, tell UI to navigate to the required screen
emit(NavigateToTncRequiredScreen());
}
}
Future<void> onTncDialogResult(
bool agreed, AuthToken authToken, List<User> users) async {
if (agreed) {
try {
await _authRepository.acceptTnc();
// The user is NOT fully authenticated yet. Just check for MPIN.
await _checkMpinAndNavigate(users);
} catch (e) {
emit(AuthError('Failed to accept TNC: $e'));
}
} else {
emit(NavigateToTncRequiredScreen());
}
}
void mpinSetupCompleted() {
if (state is NavigateToMpinSetupScreen) {
final users = (state as NavigateToMpinSetupScreen).users;
emit(Authenticated(users));
} else {
// Handle unexpected state if necessary
emit(AuthError("Invalid state during MPIN setup completion."));
}
}
Future<void> _checkMpinAndNavigate(List<User> users) async {
final mpin = await _secureStorage.read('mpin');
if (mpin == null) {
// No MPIN, tell UI to navigate to MPIN setup, carrying user data
emit(NavigateToMpinSetupScreen(users));
} else {
// MPIN exists, user is authenticated
emit(Authenticated(users));
}
}
Future<void> _checkMpinAndNavigate() async {
final mpin = await _secureStorage.read('mpin');
if (mpin == null) {
// No MPIN, tell UI to navigate to MPIN setup
emit(NavigateToMpinSetupScreen());
} else {
// MPIN exists, tell UI to navigate to the dashboard
emit(NavigateToDashboardScreen());
}
}
}

View File

@@ -46,6 +46,13 @@ class ShowTncDialog extends AuthState {
// States to trigger specific navigations from the UI
class NavigateToTncRequiredScreen extends AuthState {}
class NavigateToMpinSetupScreen extends AuthState {}
class NavigateToMpinSetupScreen extends AuthState {
final List<User> users;
const NavigateToMpinSetupScreen(this.users);
@override
List<Object> get props => [users];
}
class NavigateToDashboardScreen extends AuthState {}

View File

@@ -61,8 +61,21 @@ class AuthToken extends Equatable {
return false;
}
// Assuming 'tnc' is directly a boolean in the JWT payload
return payloadMap['tnc'] as bool;
final tncValue = payloadMap['tnc'];
// Handle different representations of 'true'
if (tncValue is bool) {
return tncValue;
}
if (tncValue is String) {
return tncValue.toLowerCase() == 'true';
}
if (tncValue is int) {
return tncValue == 1;
}
// Default to false for any other case
return false;
} catch (e) {
log('Error decoding tnc from token: $e');
// Default to false if decoding fails or 'tnc' is not found/invalid

View File

@@ -41,51 +41,236 @@ class LoginScreenState extends State<LoginScreen>
@override
Widget build(BuildContext context) {
return Scaffold(
// return Scaffold(
// body: BlocConsumer<AuthCubit, AuthState>(
// listener: (context, state) async {
// if (state is ShowTncDialog) {
// // The dialog now returns a boolean for the 'disagree' case,
// // or it completes when the 'proceed' action is finished.
// final agreed = await showDialog<bool>(
// context: context,
// barrierDismissible: false,
// builder: (dialogContext) => TncDialog(
// onProceed: () async {
// // This function is passed to the dialog.
// // It calls the cubit and completes when the cubit's work is done.
// await context
// .read<AuthCubit>()
// .onTncDialogResult(true, state.authToken, state.users);
// },
// ),
// );
// // If 'agreed' is false, it means the user clicked 'Disagree'.
// if (agreed == false) {
// if (!context.mounted) return;
// context
// .read<AuthCubit>()
// .onTncDialogResult(false, state.authToken, state.users);
// }
// } else if (state is NavigateToTncRequiredScreen) {
// Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
// } else if (state is NavigateToMpinSetupScreen) {
// Navigator.of(context).push( // Use push, NOT pushReplacement
// MaterialPageRoute(
// builder: (_) => MPinScreen(
// mode: MPinMode.set,
// onCompleted: (_) {
// // This clears the entire stack and pushes the dashboard
// Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
// MaterialPageRoute(builder: (_) => const NavigationScaffold()),
// (route) => false,
// );
// },
// ),
// ),
// );
// } else if (state is NavigateToDashboardScreen) {
// Navigator.of(context).pushReplacement(
// MaterialPageRoute(builder: (_) => const NavigationScaffold()),
// );
// } else if (state is AuthError) {
// if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
// Navigator.of(context).push(MaterialPageRoute(
// builder: (_) => SetPasswordScreen(
// customerNo: _customerNumberController.text.trim(),
// )));
// } else {
// ScaffoldMessenger.of(context)
// .showSnackBar(SnackBar(content: Text(state.message)));
// }
// }
// },
// builder: (context, state) {
// // The commented out section is removed for clarity, the logic is now above.
// return Padding(
// padding: const EdgeInsets.all(24.0),
// child: Form(
// key: _formKey,
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Image.asset(
// 'assets/images/logo.png',
// width: 150,
// height: 150,
// errorBuilder: (context, error, stackTrace) {
// return Icon(
// Icons.account_balance,
// size: 100,
// color: Theme.of(context).primaryColor,
// );
// },
// ),
// const SizedBox(height: 16),
// Text(
// AppLocalizations.of(context).kccb,
// style: TextStyle(
// fontSize: 32,
// fontWeight: FontWeight.bold,
// color: Theme.of(context).primaryColor,
// ),
// ),
// const SizedBox(height: 48),
// TextFormField(
// controller: _customerNumberController,
// decoration: InputDecoration(
// labelText: AppLocalizations.of(context).customerNumber,
// border: const OutlineInputBorder(),
// isDense: true,
// filled: true,
// fillColor: Theme.of(context).scaffoldBackgroundColor,
// enabledBorder: OutlineInputBorder(
// borderSide: BorderSide(
// color: Theme.of(context).colorScheme.outline),
// ),
// focusedBorder: OutlineInputBorder(
// borderSide: BorderSide(
// color: Theme.of(context).colorScheme.primary,
// width: 2),
// ),
// ),
// keyboardType: TextInputType.number,
// textInputAction: TextInputAction.next,
// validator: (value) {
// if (value == null || value.isEmpty) {
// return AppLocalizations.of(context).pleaseEnterUsername;
// }
// return null;
// },
// ),
// const SizedBox(height: 24),
// TextFormField(
// controller: _passwordController,
// obscureText: _obscurePassword,
// textInputAction: TextInputAction.done,
// onFieldSubmitted: (_) => _submitForm(),
// decoration: InputDecoration(
// labelText: AppLocalizations.of(context).password,
// border: const OutlineInputBorder(),
// isDense: true,
// filled: true,
// fillColor: Theme.of(context).scaffoldBackgroundColor,
// enabledBorder: OutlineInputBorder(
// borderSide: BorderSide(
// color: Theme.of(context).colorScheme.outline),
// ),
// focusedBorder: OutlineInputBorder(
// borderSide: BorderSide(
// color: Theme.of(context).colorScheme.primary,
// width: 2),
// ),
// suffixIcon: IconButton(
// icon: Icon(
// _obscurePassword
// ? Icons.visibility
// : Icons.visibility_off,
// ),
// onPressed: () {
// setState(() {
// _obscurePassword = !_obscurePassword;
// });
// },
// ),
// ),
// validator: (value) {
// if (value == null || value.isEmpty) {
// return AppLocalizations.of(context).pleaseEnterPassword;
// }
// return null;
// },
// ),
// const SizedBox(height: 24),
// SizedBox(
// width: 250,
// child: ElevatedButton(
// onPressed: state is AuthLoading ? null : _submitForm,
// style: ElevatedButton.styleFrom(
// shape: const StadiumBorder(),
// padding: const EdgeInsets.symmetric(vertical: 16),
// backgroundColor:
// Theme.of(context).scaffoldBackgroundColor,
// foregroundColor: Theme.of(context).primaryColorDark,
// side: BorderSide(
// color: Theme.of(context).colorScheme.outline,
// width: 1),
// elevation: 0,
// ),
// child: state is AuthLoading
// ? const CircularProgressIndicator()
// : Text(
// AppLocalizations.of(context).login,
// style: TextStyle(
// color: Theme.of(context)
// .colorScheme
// .onPrimaryContainer),
// ),
// ),
// ),
// const SizedBox(height: 25),
// ],
// ),
// ),
// );
// },
// ),
// );
return Scaffold(
body: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) async {
listener: (context, state) {
if (state is ShowTncDialog) {
// The dialog now returns a boolean for the 'disagree' case,
// or it completes when the 'proceed' action is finished.
final agreed = await showDialog<bool>(
showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) => TncDialog(
onProceed: () async {
// This function is passed to the dialog.
// It calls the cubit and completes when the cubit's work is done.
// Pop the dialog before the cubit action
Navigator.of(dialogContext).pop();
await context
.read<AuthCubit>()
.onTncDialogResult(true, state.authToken, state.users);
},
),
);
// If 'agreed' is false, it means the user clicked 'Disagree'.
if (agreed == false) {
if (!context.mounted) return;
context
.read<AuthCubit>()
.onTncDialogResult(false, state.authToken, state.users);
}
} else if (state is NavigateToTncRequiredScreen) {
Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
} else if (state is NavigateToMpinSetupScreen) {
Navigator.of(context).pushReplacement(
Navigator.of(context).push( // Use push, NOT pushReplacement
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
onCompleted: (_) {
Navigator.of(context, rootNavigator: true).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
// Call the cubit to signal MPIN setup is complete
context.read<AuthCubit>().mpinSetupCompleted();
},
),
),
);
} else if (state is NavigateToDashboardScreen) {
Navigator.of(context).pushReplacement(
} else if (state is Authenticated) {
// This is the single source of truth for navigating to the dashboard
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
} else if (state is AuthError) {
if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
@@ -100,7 +285,7 @@ class LoginScreenState extends State<LoginScreen>
}
},
builder: (context, state) {
// The commented out section is removed for clarity, the logic is now above.
// The builder part remains largely the same, focusing on UI display
return Padding(
padding: const EdgeInsets.all(24.0),
child: Form(

View File

@@ -185,19 +185,16 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
),
);
break;
case MPinMode.confirm:
if (widget.initialPin == pin) {
// 1) persist the pin
await storage.write('mpin', pin);
case MPinMode.confirm:
if (widget.initialPin == pin) {
// 1) persist the pin
await storage.write('mpin', pin);
// 3) now clear the entire navigation stack and go to your main scaffold
if (mounted) {
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
}
} else {
// 2) Call the onCompleted callback to let the parent handle navigation
if (mounted) {
widget.onCompleted?.call(pin);
}
} else {
setState(() {
_isError = true;
errorText = AppLocalizations.of(context).pinsDoNotMatch;