saml flow

This commit is contained in:
Surendiran 2026-05-19 12:30:28 +05:30
parent 750a3becad
commit 1288a67256
34 changed files with 4281 additions and 1179 deletions

View File

@ -19,12 +19,12 @@ if (project.hasProperty('google-services.json')) {
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '58' flutterVersionCode = '61'
} }
def flutterVersionName = localProperties.getProperty('flutter.versionName') def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) { if (flutterVersionName == null) {
flutterVersionName = '2.0.20' flutterVersionName = '2.0.23'
} }
def keystoreProperties = new Properties() def keystoreProperties = new Properties()

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

View File

@ -43,16 +43,19 @@ class Environment {
} }
} }
// static String get chatbotUrl { /// SAML IdP entry (`/saml/login?email=`). Web: same-tab redirect. Mobile: WebView.
// switch (flavor) { static String get SsoLogin {
// case Flavor.dev: switch (flavor) {
// return "https://venbait.in/nhance/dev/chatbot"; case Flavor.dev:
// case Flavor.uat: return "https://venbait.in/saml/login";
// return "https://uat.nhanceindia.in/zenith/chatbot"; case Flavor.uat:
// case Flavor.prod: return "https://appstage.nhanceindia.in/apex/saml/login";
// return "https://app.nhanceindia.in/zenith/chatbot"; case Flavor.prod:
// } return "https://app.nhanceindia.in/saml/login";
// } case Flavor.prod1:
return "https://app.nhanceindia.in/saml/login";
}
}
static String get baseHref { static String get baseHref {
switch (flavor) { switch (flavor) {

View File

@ -1,16 +1,16 @@
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart' show debugPrint, kDebugMode;
import 'package:nhance_app_pwa/config/environment.dart';
String _timestamp() => DateTime.now().toIso8601String(); String _timestamp() => DateTime.now().toIso8601String();
enum LogLevel { debug, info, warning, error } enum LogLevel { debug, info, warning, error }
class LoggerConfig { class LoggerConfig {
// Logging is always disabled in non-debug builds.
static bool enabled = true; static bool enabled = true;
// Keep debug logs off by default to avoid local slowdown on noisy screens. /// Logs below this level are dropped ([logDebug] when this is [LogLevel.info]).
static LogLevel minLevel = LogLevel.info; static LogLevel minLevel = LogLevel.info;
// Stack parsing is expensive; keep it disabled unless specifically needed. // Stack parsing is expensive; keep it disabled unless specifically needed.
@ -42,8 +42,20 @@ void _log(
Object? error, Object? error,
StackTrace? stackTrace, StackTrace? stackTrace,
}) { }) {
if (!kDebugMode || !LoggerConfig.enabled) return; if (!LoggerConfig.enabled) return;
if (level.index < LoggerConfig.minLevel.index) return;
// Verbose minimum: UAT (all modes), or local Dev when using a debug Flutter build.
final LogLevel effectiveMinLevel = switch (Environment.flavor) {
Flavor.uat => LogLevel.debug,
Flavor.dev when kDebugMode => LogLevel.debug,
_ => LoggerConfig.minLevel,
};
if (level.index < effectiveMinLevel.index) return;
// Debug lines: debug Flutter build (typical `flutter run`), or any UAT build.
if (level == LogLevel.debug && !kDebugMode && Environment.flavor != Flavor.uat) {
return;
}
final inferredTag = tag ?? final inferredTag = tag ??
(LoggerConfig.includeCallerFromStack (LoggerConfig.includeCallerFromStack
@ -58,8 +70,21 @@ void _log(
error: error, error: error,
stackTrace: stackTrace, stackTrace: stackTrace,
); );
// `developer.log` is easy to miss in release/profile (IDE filters, logcat tags).
// UAT: always mirror to stdout-style logging so `adb logcat`, Xcode, and web consoles show lines.
// Debug builds: mirror too for parity with `print` during local runs.
if (Environment.flavor == Flavor.uat || kDebugMode) {
debugPrint(fullMessage);
if (Environment.flavor == Flavor.uat && (error != null || stackTrace != null)) {
debugPrint('$error\n$stackTrace');
}
}
} }
/// **Dev + localhost:** shown when you use `flutter run` (debug mode); not in `flutter run --release`.
/// **UAT:** also emitted in profile/release (verbose).
/// **Prod / Prod1:** debug Flutter build only; in release/profile use [logInfo] or `print`.
void logDebug(Object? message, {String? tag}) { void logDebug(Object? message, {String? tag}) {
_log(LogLevel.debug, message, tag: tag); _log(LogLevel.debug, message, tag: tag);
} }

View File

@ -12,6 +12,7 @@ import 'package:nhance_app_pwa/pages/enrollment/addons.dart';
import 'package:nhance_app_pwa/pages/enrollment/empDetails.dart'; import 'package:nhance_app_pwa/pages/enrollment/empDetails.dart';
import 'package:nhance_app_pwa/pages/enrollment/empReview.dart'; import 'package:nhance_app_pwa/pages/enrollment/empReview.dart';
import 'package:nhance_app_pwa/pages/login.dart'; import 'package:nhance_app_pwa/pages/login.dart';
import 'package:nhance_app_pwa/pages/sso_login_return_page.dart';
import 'package:nhance_app_pwa/pages/postEnrollment/AddPolicyScreen.dart'; import 'package:nhance_app_pwa/pages/postEnrollment/AddPolicyScreen.dart';
import 'package:nhance_app_pwa/pages/postEnrollment/claimprocess.dart'; import 'package:nhance_app_pwa/pages/postEnrollment/claimprocess.dart';
import 'package:nhance_app_pwa/pages/postEnrollment/claims.dart'; import 'package:nhance_app_pwa/pages/postEnrollment/claims.dart';
@ -98,6 +99,7 @@ Future<String?> tokenRedirectLogic(
logDebug('ABCDEFGH'); logDebug('ABCDEFGH');
const guestRoutes = [ const guestRoutes = [
'/login', '/login',
'/sso-login',
'/verify', '/verify',
'/mailVerify', '/mailVerify',
'/pinPage', '/pinPage',
@ -107,21 +109,32 @@ Future<String?> tokenRedirectLogic(
final hasToken = await TokenService.hasValidToken(); final hasToken = await TokenService.hasValidToken();
logDebug('hasToken : $hasToken'); logDebug('hasToken : $hasToken');
final location = state.matchedLocation; final location = state.matchedLocation;
final session = SessionManager();
logDebug('Location: $location'); logDebug('Location: $location');
logDebug('hasToken: $hasToken'); logDebug('hasToken: $hasToken');
// Already logged in prevent guest pages if (hasToken) {
await session.restoreSession();
}
if (!kIsWeb && hasToken) {
final reauth = await _mobileReauthRedirectIfNeeded(location);
if (reauth != null) return reauth;
}
// Logged in on a guest route home (only when mobile session is unlocked).
if (hasToken && guestRoutes.contains(location)) { if (hasToken && guestRoutes.contains(location)) {
if (!kIsWeb && !session.isMobileAppSessionUnlocked) {
return null;
}
return '/home'; return '/home';
} }
// Not logged in allow guest pages directly
if (!hasToken && guestRoutes.contains(location)) { if (!hasToken && guestRoutes.contains(location)) {
return null; return null;
} }
// Protected route without token
if (!hasToken && !guestRoutes.contains(location)) { if (!hasToken && !guestRoutes.contains(location)) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ToastHelper.showErrorToast( ToastHelper.showErrorToast(
@ -130,15 +143,56 @@ Future<String?> tokenRedirectLogic(
return '/login'; return '/login';
} }
// Restore session (decode JWT etc.) return null;
await SessionManager().restoreSession(); }
return null; // no redirect /// After a full app restart (new process), require MPIN or password login before
/// protected routes. Tokens remain in secure storage. Minimizing does not lock.
Future<String?> _mobileReauthRedirectIfNeeded(String location) async {
const allowedWhileLocked = <String>{
'/login',
'/pinPage',
'/verify',
'/mailVerify',
'/sso-login',
'/pinSettingPage',
'/changePin',
};
final session = SessionManager();
if (session.isMobileAppSessionUnlocked) return null;
if (allowedWhileLocked.contains(location)) return null;
final prefs = await SharedPreferences.getInstance();
final skip = prefs.getString('is_mpin_skipped');
return skip == '0' ? '/pinPage' : '/login';
} }
/// When not using `-t lib/config/main_uat.dart`, pass e.g. `--dart-define=APP_FLAVOR=uat`
/// so [Environment.flavor] (and logger UAT rules) match the build.
void _applyAppFlavorFromDartDefine() {
const fromDefine = String.fromEnvironment('APP_FLAVOR', defaultValue: '');
switch (fromDefine.toLowerCase()) {
case 'uat':
Environment.flavor = Flavor.uat;
break;
case 'prod':
Environment.flavor = Flavor.prod;
break;
case 'prod1':
Environment.flavor = Flavor.prod1;
break;
case 'dev':
Environment.flavor = Flavor.dev;
break;
default:
break;
}
}
Future<void> startApp() async { Future<void> startApp() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
_applyAppFlavorFromDartDefine();
await TokenService.clearLegacyWebAuthFromSharedPreferences(); await TokenService.clearLegacyWebAuthFromSharedPreferences();
@ -156,8 +210,11 @@ Future<void> startApp() async {
routes: [ routes: [
GoRoute( GoRoute(
path: '/splash', builder: (context, state) => const SplashScreen()), path: '/splash', builder: (context, state) => const SplashScreen()),
GoRoute(path: '/login', builder: (context, state) => login()), GoRoute(path: '/login', builder: (context, state) => const login()),
GoRoute(
path: '/sso-login',
builder: (context, state) => const SsoLoginReturnPage(),
),
GoRoute( GoRoute(
path: '/mailVerify', path: '/mailVerify',
@ -179,57 +236,74 @@ Future<void> startApp() async {
path: '/changePin', path: '/changePin',
builder: (context, state) => changePin(), builder: (context, state) => changePin(),
), ),
GoRoute(path: '/home', builder: (context, state) => Home()),
GoRoute(path: '/profile', builder: (context, state) => profile()),
GoRoute(path: '/help', builder: (context, state) => help()),
GoRoute( GoRoute(
path: '/claimprocess', builder: (context, state) => claimprocess()), path: '/home',
GoRoute(path: '/wellness', builder: (context, state) => wellness()), builder: (context, state) => ChatbotHost(
child: Home(),
triggerIntroOnHome: true,
),
),
GoRoute(
path: '/profile',
builder: (context, state) => ChatbotHost(child: profile()),
),
GoRoute(
path: '/help',
builder: (context, state) => ChatbotHost(child: help()),
),
GoRoute(
path: '/claimprocess', builder: (context, state) => ChatbotHost(child: claimprocess()),
),
GoRoute(path: '/wellness', builder: (context, state) => ChatbotHost(child:wellness()),
),
GoRoute( GoRoute(
path: '/privacypolicy', builder: (context, state) => privacypolicy()), path: '/privacypolicy', builder: (context, state) => privacypolicy()),
GoRoute(path: '/termsofuse', builder: (context, state) => termsofuse()), GoRoute(path: '/termsofuse', builder: (context, state) => termsofuse()),
GoRoute( GoRoute(
path: '/generalExclusionsDeductibles', path: '/generalExclusionsDeductibles',
builder: (context, state) => generalExclusionsDeductibles()), builder: (context, state) => ChatbotHost(child:generalExclusionsDeductibles()),
),
GoRoute(path: '/tickets', builder: (context, state) => tickets()), GoRoute(path: '/tickets', builder: (context, state) => ChatbotHost(child:tickets()),
),
GoRoute( GoRoute(
path: '/policies', path: '/policies',
builder: (context, state) { builder: (context, state) {
final arguments = final arguments =
state.extra as Map<String, dynamic>?; // 👈 receive here state.extra as Map<String, dynamic>?; // 👈 receive here
return policies(arguments: arguments); return ChatbotHost(child:policies(arguments: arguments));
}, },
), ),
GoRoute( GoRoute(
path: '/claims', path: '/claims',
builder: (context, state) { builder: (context, state) {
final int tabIndex = state.extra as int? ?? 0; // default to 0 final int tabIndex = state.extra as int? ?? 0; // default to 0
return claims(initialTab: tabIndex); return ChatbotHost(child: claims(initialTab: tabIndex));
}, },
), ),
GoRoute( GoRoute(
path: '/planclaimsform', path: '/planclaimsform',
builder: (context, state) { builder: (context, state) {
final details = state.extra as Map<String, dynamic>?; final details = state.extra as Map<String, dynamic>?;
return planclaimsform(details: details); return ChatbotHost(child: planclaimsform(details: details));
}, },
), ),
GoRoute( GoRoute(
path: '/retailClaimForm', path: '/retailClaimForm',
builder: (context, state) { builder: (context, state) {
final details = state.extra as Map<String, dynamic>?; final details = state.extra as Map<String, dynamic>?;
return retailClaimForm(details: details); return ChatbotHost(child:retailClaimForm(details: details));
}, },
), ),
GoRoute( GoRoute(
path: '/raisedTicketHistory', path: '/raisedTicketHistory',
builder: (context, state) => raisedTicketHistory()), builder: (context, state) => ChatbotHost(child:raisedTicketHistory()),
),
GoRoute( GoRoute(
path: '/tickettracklist/:ticketID', path: '/tickettracklist/:ticketID',
builder: (context, state) { builder: (context, state) {
final ticketID = state.pathParameters['ticketID']!; final ticketID = state.pathParameters['ticketID']!;
return tickettracklist(ticketID: ticketID); return ChatbotHost(child:tickettracklist(ticketID: ticketID));
}, },
), ),
// GoRoute( // GoRoute(
@ -245,7 +319,7 @@ Future<void> startApp() async {
), ),
GoRoute( GoRoute(
path: '/faqs', path: '/faqs',
builder: (context, state) => faqs(), builder: (context, state) => ChatbotHost(child: faqs()),
), ),
GoRoute( GoRoute(
path: '/addOnsDetails', builder: (context, state) => addOnsDetails()), path: '/addOnsDetails', builder: (context, state) => addOnsDetails()),
@ -294,7 +368,7 @@ Future<void> startApp() async {
// Middleware hook // Middleware hook
redirect: (context, state) async => redirect: (context, state) async =>
await tokenRedirectLogic(context, state), await tokenRedirectLogic(context, state),
); );
runApp( runApp(
@ -332,4 +406,4 @@ class _MyAppState extends State<MyApp> {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
); );
} }
} }

View File

@ -173,7 +173,7 @@ class _changesPasswordState extends State<changesPassword> {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
// ToastHelper.showErrorToast(context, 'Something went wrong'); // ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
logDebug('Error: $e'); logDebug('Error: $e');
} }

View File

@ -307,29 +307,42 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
if (!context.mounted) return; if (!context.mounted) return;
context.go('/login'); context.go('/login');
} else if (response.statusCode == 403) {
setState(() {
_isLoading = false;
});
await SessionManager().clear();
ToastHelper.showErrorToast(context, 'Session Out');
if (!context.mounted) return;
context.go('/login');
} else if (response.statusCode == 451) {
setState(() {
_isLoading = false;
});
final body = jsonDecode(response.body);
final message = body['message'];
ToastHelper.showWarningToast(context, message);
} else if (response.statusCode == 429) { } else if (response.statusCode == 429) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
final body = jsonDecode(response.body); Map<String, dynamic> data = json.decode(response.body);
final message = body['message']; if (data.containsKey('error')) {
ToastHelper.showWarningToast(context, message); final message = data['error']['message'];
ToastHelper.showErrorToast(context, message);
} else {
final message = data['message'];
ToastHelper.showErrorToast(context, message);
}
} else if (response.statusCode == 403) {
setState(() {
_isLoading = false;
});
Map<String, dynamic> data = json.decode(response.body);
if (data.containsKey('error')) {
final message = data['error']['message'];
ToastHelper.showErrorToast(context, message);
} else {
final message = data['message'];
ToastHelper.showErrorToast(context, message);
}
} else if (response.statusCode == 451) {
setState(() {
_isLoading = false;
});
Map<String, dynamic> data = json.decode(response.body);
if (data.containsKey('error')) {
final message = data['error']['message'];
ToastHelper.showErrorToast(context, message);
} else {
final message = data['message'];
ToastHelper.showErrorToast(context, message);
}
} else { } else {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@ -340,7 +353,7 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.clear(); prefs.clear();
logDebug('Error: $e'); logDebug('Error: $e');
ToastHelper.showWarningToast(context, 'Something went wrong'); ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
// Show a Snackbar if there's an error while verifying OTP // Show a Snackbar if there's an error while verifying OTP
logDebug('Failed to verify OTP. Please try again.'); logDebug('Failed to verify OTP. Please try again.');
} finally { } finally {
@ -630,12 +643,48 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
ToastHelper.showErrorToast(context, message); ToastHelper.showErrorToast(context, message);
logDebug('Invalid mobile number'); logDebug('Invalid mobile number');
} }
} else if (response.statusCode == 429) {
setState(() {
_isLoading = false;
});
Map<String, dynamic> data = json.decode(response.body);
if (data.containsKey('error')) {
final message = data['error']['message'];
ToastHelper.showErrorToast(context, message);
} else {
final message = data['message'];
ToastHelper.showErrorToast(context, message);
}
} else if (response.statusCode == 403) {
setState(() {
_isLoading = false;
});
Map<String, dynamic> data = json.decode(response.body);
if (data.containsKey('error')) {
final message = data['error']['message'];
ToastHelper.showErrorToast(context, message);
} else {
final message = data['message'];
ToastHelper.showErrorToast(context, message);
}
} else if (response.statusCode == 451) {
setState(() {
_isLoading = false;
});
Map<String, dynamic> data = json.decode(response.body);
if (data.containsKey('error')) {
final message = data['error']['message'];
ToastHelper.showErrorToast(context, message);
} else {
final message = data['message'];
ToastHelper.showErrorToast(context, message);
}
} else { } else {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
throw Exception('Failed to verify mobile number'); throw Exception('Failed to verify mobile number');
} }
} catch (e) { } catch (e) {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
logDebug('Error: $e'); logDebug('Error: $e');
} }
} }
@ -689,10 +738,12 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
if (_postToken != null && _postToken.isNotEmpty) { if (_postToken != null && _postToken.isNotEmpty) {
ToastHelper.showSuccessToast(context, 'Successfully Logged In'); ToastHelper.showSuccessToast(context, 'Successfully Logged In');
SessionManager().unlockMobileAppSession();
context.replace('/home'); context.replace('/home');
// context.go('/home'); // context.go('/home');
// Navigator.pushReplacementNamed(context, 'home'); // Navigator.pushReplacementNamed(context, 'home');
} else { } else {
SessionManager().unlockMobileAppSession();
context.replace('/empDetails'); context.replace('/empDetails');
// context.go('/empDetails'); // context.go('/empDetails');
// Navigator.pushReplacementNamed(context, 'empDetails'); // Navigator.pushReplacementNamed(context, 'empDetails');
@ -716,12 +767,48 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
context.go('/pinSettingPage'); context.go('/pinSettingPage');
// Navigator.pushReplacementNamed(context, 'pinSettingPage'); // Navigator.pushReplacementNamed(context, 'pinSettingPage');
} }
} else if (response.statusCode == 429) {
setState(() {
_isLoading = false;
});
Map<String, dynamic> data = json.decode(response.body);
if (data.containsKey('error')) {
final message = data['error']['message'];
ToastHelper.showErrorToast(context, message);
} else {
final message = data['message'];
ToastHelper.showErrorToast(context, message);
}
} else if (response.statusCode == 403) {
setState(() {
_isLoading = false;
});
Map<String, dynamic> data = json.decode(response.body);
if (data.containsKey('error')) {
final message = data['error']['message'];
ToastHelper.showErrorToast(context, message);
} else {
final message = data['message'];
ToastHelper.showErrorToast(context, message);
}
} else if (response.statusCode == 451) {
setState(() {
_isLoading = false;
});
Map<String, dynamic> data = json.decode(response.body);
if (data.containsKey('error')) {
final message = data['error']['message'];
ToastHelper.showErrorToast(context, message);
} else {
final message = data['message'];
ToastHelper.showErrorToast(context, message);
}
} else { } else {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
throw Exception('Failed to verify pin number'); throw Exception('Failed to verify pin number');
} }
} catch (e) { } catch (e) {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
logDebug('Error: $e'); logDebug('Error: $e');
} }
} }
@ -760,11 +847,11 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
); );
} }
} else { } else {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
throw Exception('Failed to verify pin number'); throw Exception('Failed to verify pin number');
} }
} catch (e) { } catch (e) {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
logDebug('Error: $e'); logDebug('Error: $e');
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -347,7 +347,7 @@ class _empDetailsState extends State<empDetails> {
gpaDataIsEmpty = 0; gpaDataIsEmpty = 0;
}); });
// Handle other status codes // Handle other status codes
ToastHelper.showWarningToast(context, 'Something went wrong'); ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
logDebug('Request failed with status: ${response['code']}'); logDebug('Request failed with status: ${response['code']}');
} }
} catch (e) { } catch (e) {
@ -1161,6 +1161,181 @@ class _empDetailsState extends State<empDetails> {
logDebug("Normalized relationship list: $relationshipObjects"); logDebug("Normalized relationship list: $relationshipObjects");
Future<bool> showEditConfirmationDialog(
BuildContext dialogContext,
Map<String, dynamic> oldData,
Map<String, dynamic> newData,
bool relationshipEnabled,
) async {
final oldRelationship = (oldData["relationship"] ?? "").toString().trim();
final oldName = (oldData["name"] ?? "").toString().trim();
final oldDob = (oldData["dob"] ?? "").toString().trim();
final newRelationship = (newData["relationship"] ?? "").toString().trim();
final newName = (newData["memberName"] ?? "").toString().trim();
final newDob = (newData["dateOfBirth"] ?? "").toString().trim();
final List<Map<String, String>> changedFields = [];
if (oldRelationship != newRelationship) {
changedFields.add({
"label": "Relationship",
"old": oldRelationship,
"new": newRelationship,
});
}
if (oldName != newName) {
changedFields.add({
"label": "Member Name",
"old": oldName,
"new": newName,
});
}
if (oldDob != newDob) {
changedFields.add({
"label": "Date of Birth",
"old": oldDob,
"new": newDob,
});
}
if (changedFields.isEmpty) {
ToastHelper.showWarningToast(
dialogContext, "No changes detected to save");
return false;
}
final List<Map<String, String>> reviewFields = [];
if (relationshipEnabled || oldRelationship != newRelationship) {
reviewFields.add({
"label": "Relationship",
"old": oldRelationship,
"new": newRelationship,
});
}
reviewFields.add({
"label": "Member Name",
"old": oldName,
"new": newName,
});
reviewFields.add({
"label": "Date of Birth",
"old": oldDob,
"new": newDob,
});
final bool? confirmed = await showDialog<bool>(
context: dialogContext,
barrierDismissible: false,
builder: (confirmContext) {
return AlertDialog(
title: Text(
"Confirm Changes",
style: GoogleFonts.poppins(fontWeight: FontWeight.w600),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Please review old and new values before saving:",
style: GoogleFonts.poppins(fontSize: 13),
),
SizedBox(height: 12),
...reviewFields.map((field) {
final oldValue = (field["old"] ?? '').trim().isEmpty
? '-'
: (field["old"] ?? '');
final newValue = (field["new"] ?? '').trim().isEmpty
? '-'
: (field["new"] ?? '');
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFFF9F9F9),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE0E0E0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
field["label"] ?? '',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w700,
color: const Color(0xFFE26728),
),
),
const SizedBox(height: 8),
RichText(
text: TextSpan(
style: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF232526),
),
children: [
const TextSpan(
text: "OLD: ",
style: TextStyle(
fontWeight: FontWeight.w700,
color: Color(0xFFD32F2F),
),
),
TextSpan(text: oldValue),
],
),
),
const SizedBox(height: 4),
RichText(
text: TextSpan(
style: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF232526),
),
children: [
const TextSpan(
text: "NEW: ",
style: TextStyle(
fontWeight: FontWeight.w700,
color: Color(0xFF2E7D32),
),
),
TextSpan(text: newValue),
],
),
),
],
),
),
);
}).toList(),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(confirmContext, false),
child: Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFE26728),
),
onPressed: () => Navigator.pop(confirmContext, true),
child: Text("Confirm", style: TextStyle(color: Colors.white)),
),
],
);
},
);
return confirmed == true;
}
// Reset UI Fields // Reset UI Fields
_relationShipController.clear(); _relationShipController.clear();
_memberNameController.clear(); _memberNameController.clear();
@ -1323,7 +1498,7 @@ class _empDetailsState extends State<empDetails> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFE26728), backgroundColor: Color(0xFFE26728),
), ),
onPressed: () { onPressed: () async {
if (_memberNameController.text.isEmpty || if (_memberNameController.text.isEmpty ||
_dobController.text.isEmpty || _dobController.text.isEmpty ||
dropdownValue == null) { dropdownValue == null) {
@ -1342,6 +1517,18 @@ class _empDetailsState extends State<empDetails> {
logDebug(floaterData); logDebug(floaterData);
logDebug(selectedFloaterData); logDebug(selectedFloaterData);
if (action == "Edit" && floaterData != null) {
final shouldProceed = await showEditConfirmationDialog(
context,
floaterData,
formData,
action != "Edit",
);
if (!shouldProceed) {
return;
}
}
saveFamilyMemberDetails( saveFamilyMemberDetails(
formData, formData,
selectedFloaterData ?? floaterData ?? {}, selectedFloaterData ?? floaterData ?? {},

View File

@ -32,6 +32,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
List<String> typeOrder = [ List<String> typeOrder = [
'GPA', 'GPA',
'GMC', 'GMC',
'GMC - OPD',
'GMC - Parents', 'GMC - Parents',
'GMC - Topup', 'GMC - Topup',
'GMC - Topup(Parents)' 'GMC - Topup(Parents)'
@ -111,10 +112,13 @@ class _empReviewDetailsState extends State<empReviewDetails> {
dynamic topUpSiSelectedSI; dynamic topUpSiSelectedSI;
dynamic topUpSumInsured; dynamic topUpSumInsured;
dynamic topUpFloaterTextHeading; dynamic topUpFloaterTextHeading;
dynamic opdFloaterTextHeading;
dynamic topUpECardDownload; dynamic topUpECardDownload;
bool topUpIsPremiumSummary = false; bool topUpIsPremiumSummary = false;
dynamic topUpDisclaimer; dynamic topUpDisclaimer;
int showHideTopUpCard = 0; int showHideTopUpCard = 0;
int showHideOpdCard = 0;
int showHideAddOnsCard = 0; int showHideAddOnsCard = 0;
int showHideTopUpParentCard = 0; int showHideTopUpParentCard = 0;
dynamic iAgreeForAddOn = []; dynamic iAgreeForAddOn = [];
@ -133,7 +137,25 @@ class _empReviewDetailsState extends State<empReviewDetails> {
dynamic topUpParentFloterTextHeading; dynamic topUpParentFloterTextHeading;
dynamic topUpParentDisclaimer; dynamic topUpParentDisclaimer;
// int topUpParentOpenForEnrollment = 0; // int topUpParentOpenForEnrollment = 0;
dynamic opdPolicies = [];
dynamic opdClientPolicyId;
dynamic opdSlabRates;
dynamic opdFamilyFloater;
dynamic opdPolicyName;
dynamic opdPolicyType;
dynamic opdMappedFamilyFloatersSi;
dynamic opdECardDownload;
int opdOpenForEnrollment = 0;
dynamic opdTypeName;
dynamic opdSiValue;
dynamic opdSiSelectedSI;
dynamic opdSiPremiumValue;
dynamic opdSiPremiumGst;
dynamic opdSiTotalAmt;
bool opdIsPremiumSummary = false;
dynamic opdDisclaimer;
int activeSiData = 0; int activeSiData = 0;
int activeOPDSiData = 0;
int activeSiParentData = 0; int activeSiParentData = 0;
int activeDependentData = 0; int activeDependentData = 0;
int activePremiumSummary = 0; int activePremiumSummary = 0;
@ -215,6 +237,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
getClientLogoAndDetails(); getClientLogoAndDetails();
} }
getGmcSiTopUp(); getGmcSiTopUp();
getGmcOPD();
getGmcSiParentTopUp(); getGmcSiParentTopUp();
getGmcDependentAddOns(); getGmcDependentAddOns();
getGpaEmpPolicyDetails(enrollmentEmpPrimaryId); getGpaEmpPolicyDetails(enrollmentEmpPrimaryId);
@ -268,6 +291,11 @@ class _empReviewDetailsState extends State<empReviewDetails> {
return intValue != 0; return intValue != 0;
} }
bool _isOpenForEnrollment(dynamic value) {
if (value is int) return value == 1;
return value?.toString() == '1';
}
void primarySummary() async { void primarySummary() async {
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('siData')) { if (prefs.containsKey('siData')) {
@ -286,6 +314,21 @@ class _empReviewDetailsState extends State<empReviewDetails> {
topUpSiTotalAmt = firstSiData['topUpSiTotalAmt']; topUpSiTotalAmt = firstSiData['topUpSiTotalAmt'];
} }
} }
if (prefs.containsKey('opdData')) {
activeOPDSiData = 1;
String? opdDataJson = prefs.getString('opdData');
logDebug('siDataJson');
logDebug(opdDataJson);
List<dynamic> siDataList = jsonDecode(opdDataJson!);
if (siDataList.isNotEmpty) {
// Access the first map in the list
Map<String, dynamic> firstSiData = siDataList.first;
// Get the value of topUpSiPremiumValue
opdSiPremiumValue = firstSiData['opdSiPremiumValue'];
opdSiPremiumGst = firstSiData['opdSiPremiumGst'];
opdSiTotalAmt = firstSiData['opdSiTotalAmt'];
}
}
if (prefs.containsKey('siParentData')) { if (prefs.containsKey('siParentData')) {
activeSiParentData = 1; activeSiParentData = 1;
String? siParentDataJson = prefs.getString('siParentData'); String? siParentDataJson = prefs.getString('siParentData');
@ -327,7 +370,8 @@ class _empReviewDetailsState extends State<empReviewDetails> {
logDebug(addOnsDependentTotalAmt); logDebug(addOnsDependentTotalAmt);
} }
} }
if (prefs.containsKey('siData') &&
if (prefs.containsKey('siData') && prefs.containsKey('opdData') &&
prefs.containsKey('siParentData') && prefs.containsKey('siParentData') &&
prefs.containsKey('dependentData')) { prefs.containsKey('dependentData')) {
activePremiumSummary = 0; activePremiumSummary = 0;
@ -343,9 +387,9 @@ class _empReviewDetailsState extends State<empReviewDetails> {
logDebug('gmcTotalAmt: $gmcTotalAmt'); logDebug('gmcTotalAmt: $gmcTotalAmt');
if (topUpSiTotalAmt != null) totalPayableAmt += topUpSiTotalAmt; if (topUpSiTotalAmt != null) totalPayableAmt += topUpSiTotalAmt;
if (opdSiTotalAmt != null) totalPayableAmt += opdSiTotalAmt;
if (topUpParentSiTotalAmt != null) totalPayableAmt += topUpParentSiTotalAmt; if (topUpParentSiTotalAmt != null) totalPayableAmt += topUpParentSiTotalAmt;
if (addOnsDependentTotalAmt != null) if (addOnsDependentTotalAmt != null) totalPayableAmt += addOnsDependentTotalAmt;
totalPayableAmt += addOnsDependentTotalAmt;
logDebug('Final Total Payable Amount: $totalPayableAmt'); logDebug('Final Total Payable Amount: $totalPayableAmt');
} }
@ -423,7 +467,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
for (var item in gpaPolicies) { for (var item in gpaPolicies) {
// Extract disclaimer value from each object // Extract disclaimer value from each object
if (item['OpenForEnrollment'] == '1') { if (_isOpenForEnrollment(item['OpenForEnrollment'])) {
dynamic disclaimer = item['disclaimer']; dynamic disclaimer = item['disclaimer'];
if (disclaimer != null && disclaimer.isNotEmpty) { if (disclaimer != null && disclaimer.isNotEmpty) {
// Temporary list to hold new entries // Temporary list to hold new entries
@ -472,7 +516,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
gpaDataIsEmpty = 0; gpaDataIsEmpty = 0;
}); });
// Handle other status codes // Handle other status codes
ToastHelper.showWarningToast(context, 'Something went wrong'); ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
logDebug('Request failed with status: ${response['code']}'); logDebug('Request failed with status: ${response['code']}');
} }
} catch (e) { } catch (e) {
@ -508,7 +552,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
} }
for (var item in gmcPolicies) { for (var item in gmcPolicies) {
if (item['OpenForEnrollment'] == '1') { if (_isOpenForEnrollment(item['OpenForEnrollment'])) {
dynamic disclaimer = item['disclaimer']; dynamic disclaimer = item['disclaimer'];
if (disclaimer != null && disclaimer.isNotEmpty) { if (disclaimer != null && disclaimer.isNotEmpty) {
// Temporary list to hold new entries // Temporary list to hold new entries
@ -632,7 +676,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
topUpDisclaimer = topUpDisclaimer =
await topUpSiPolicies['gmc_si_topup']['disclaimer']; await topUpSiPolicies['gmc_si_topup']['disclaimer'];
if (topUpOpenForEnrollment == '1' && activeSiData == 1) { if (_isOpenForEnrollment(topUpOpenForEnrollment) && activeSiData == 1) {
if (topUpDisclaimer != null && topUpDisclaimer.isNotEmpty) { if (topUpDisclaimer != null && topUpDisclaimer.isNotEmpty) {
// Temporary list to hold new entries // Temporary list to hold new entries
List<Map<String, dynamic>> newEntries = []; List<Map<String, dynamic>> newEntries = [];
@ -693,6 +737,128 @@ class _empReviewDetailsState extends State<empReviewDetails> {
} }
} }
Future<void> getGmcOPD() async {
try {
if (enrollmentClient_id == null ||
enrollmentEmpCodeString == null ||
enrollmentEmpClientBranchId == null) {
return;
}
final response = await apiService.getGmcSiTopUpToApi(
enrollmentClient_id!,
enrollmentEmpCodeString!,
'GMC-OPD',
enrollmentEmpClientBranchId!);
if (response['status'] == 'success') {
if (response.containsKey('data')) {
setState(() {
opdPolicies = response['data'];
});
if (opdPolicies.isNotEmpty) {
int _toInt(dynamic value) {
if (value is int) return value;
if (value is double) return value.toInt();
return int.tryParse(value?.toString() ?? '0') ?? 0;
}
final opdNode = opdPolicies['gmc_opd'];
opdClientPolicyId = opdNode['client_policy_id'];
opdSlabRates = opdNode['SlabRates'];
opdFloaterTextHeading = opdNode['floter_text_heading'];
opdPolicyName = opdNode['policy_name'];
opdPolicyType = opdNode['type'];
opdFamilyFloater = opdNode['policy_terms']['family_floater'];
opdMappedFamilyFloatersSi =
opdNode['family_floaters_of_only_si_array'];
opdECardDownload = opdNode['eCardDownload'];
opdOpenForEnrollment = _toInt(opdNode['OpenForEnrollment']);
opdTypeName = opdNode['type'];
opdSiValue = opdNode['family_floaters_of_only_si_value'];
logDebug(opdSiValue);
if (opdSiValue == 0) {
showHideOpdCard = 0;
} else {
showHideOpdCard = 1;
}
opdSiSelectedSI = opdSiValue != 0 ? opdSiValue.toString() : null;
// // OPD can have SI value 0 initially; still show card when policy exists.
// showHideOpdCard = (opdPolicyName != null ||
// opdPolicyType != null ||
// (opdMappedFamilyFloatersSi is List &&
// opdMappedFamilyFloatersSi.isNotEmpty))
// ? 1
// : 0;
setState(() {
opdSiPremiumValue =
_toInt(opdNode['family_floaters_of_only_si_premium_value']);
opdSiPremiumGst =
_toInt(opdNode['family_floaters_of_only_si_gst_value']);
opdSiTotalAmt = opdSiPremiumValue + opdSiPremiumGst;
opdIsPremiumSummary =
isPremiumEnabled(opdNode?['is_premium_summery']);
});
opdDisclaimer = opdNode['disclaimer'];
if (_isOpenForEnrollment(opdOpenForEnrollment) &&
activeOPDSiData == 1) {
if (opdDisclaimer != null && opdDisclaimer.isNotEmpty) {
// Temporary list to hold new entries
List<Map<String, dynamic>> newEntries = [];
if (opdDisclaimer is String) {
// Wrap string in a map with id and checked
newEntries.add({
'id': allDisclaimer.length + 1, // Unique ID based on length
'text': opdDisclaimer,
'checked': false,
'type': opdTypeName
});
} else if (opdDisclaimer is List) {
// Convert each string in the list to a map with id and checked
newEntries.addAll(
List<Map<String, dynamic>>.from(
opdDisclaimer.map((text) => {
'id': allDisclaimer.length + newEntries.length + 1,
'text': text,
'checked': false,
'type': opdTypeName
}),
),
);
} else {
logDebug('Unexpected type for disclaimer');
}
// Add new entries to the main list, ensuring no duplicate texts
newEntries.forEach((entry) {
if (!allDisclaimer
.any((item) => item['text'] == entry['text'])) {
allDisclaimer.add(entry);
}
});
// Sort disclaimers after adding
_sortDisclaimerByTypeOrder();
}
}
} else {
logDebug('No data found in the response');
}
} else {
logDebug('API request failed with status: ${response['status']}');
}
} else {
logDebug('Request failed with status: ${response['code']}');
}
} catch (e) {
logDebug('Exception occurred: $e');
}
}
Future<void> getGmcSiParentTopUp() async { Future<void> getGmcSiParentTopUp() async {
try { try {
if (enrollmentClient_id == null || if (enrollmentClient_id == null ||
@ -786,7 +952,7 @@ class _empReviewDetailsState extends State<empReviewDetails> {
await topUpSiParentPolicies['gmc_si_parent_topup'] await topUpSiParentPolicies['gmc_si_parent_topup']
['disclaimer']; ['disclaimer'];
if (topUpParentOpenForEnrollment == '1' && if (_isOpenForEnrollment(topUpParentOpenForEnrollment) &&
activeSiParentData == 1) { activeSiParentData == 1) {
if (topUpParentDisclaimer != null && if (topUpParentDisclaimer != null &&
topUpParentDisclaimer.isNotEmpty) { topUpParentDisclaimer.isNotEmpty) {
@ -936,7 +1102,7 @@ setState(() {
logDebug( logDebug(
'addOnsDependentOpenForEnrollment $addOnsDependentOpenForEnrollment'); 'addOnsDependentOpenForEnrollment $addOnsDependentOpenForEnrollment');
logDebug('activeDependentData $activeDependentData'); logDebug('activeDependentData $activeDependentData');
if (addOnsDependentOpenForEnrollment == '1' && if (_isOpenForEnrollment(addOnsDependentOpenForEnrollment) &&
activeDependentData == 1) { activeDependentData == 1) {
logDebug('addOnsDependentOpenForEnrollment'); logDebug('addOnsDependentOpenForEnrollment');
if (addOnsDependentDisclaimer != null && if (addOnsDependentDisclaimer != null &&
@ -1025,6 +1191,18 @@ setState(() {
} }
} }
if (opdMappedFamilyFloatersSi != null) {
dynamic getSiTrueObjects = opdMappedFamilyFloatersSi
.where((element) => element['is_value_exist'] == true)
.toList();
logDebug('getSiTrueObjects : $getSiTrueObjects');
if (getSiTrueObjects.isNotEmpty) {
iAgreeForAddOn.add(int.parse(opdClientPolicyId));
}
}
if (topUpParentMappedFamilyFloatersSi != null) { if (topUpParentMappedFamilyFloatersSi != null) {
dynamic getSiTrueObjects = topUpParentMappedFamilyFloatersSi dynamic getSiTrueObjects = topUpParentMappedFamilyFloatersSi
.where((element) => element['is_value_exist'] == true) .where((element) => element['is_value_exist'] == true)
@ -1172,7 +1350,7 @@ setState(() {
color: Colors.red, color: Colors.red,
), ),
), ),
content: Text('Something went wrong. Enrollemnt not completed'), content: Text('Unable to process. Please try again later. Enrollemnt not completed'),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text( child: Text(
@ -1251,6 +1429,7 @@ setState(() {
double total = 0.0; // Initialize to avoid unwanted accumulation double total = 0.0; // Initialize to avoid unwanted accumulation
if (topUpSiTotalAmt != null) total += topUpSiTotalAmt!; if (topUpSiTotalAmt != null) total += topUpSiTotalAmt!;
if (opdSiTotalAmt != null) total += opdSiTotalAmt!;
if (topUpParentSiTotalAmt != null) total += topUpParentSiTotalAmt!; if (topUpParentSiTotalAmt != null) total += topUpParentSiTotalAmt!;
if (addOnsDependentTotalAmt != null) total += addOnsDependentTotalAmt!; if (addOnsDependentTotalAmt != null) total += addOnsDependentTotalAmt!;
if (gpaTotalAmt != null) total += gpaTotalAmt!; if (gpaTotalAmt != null) total += gpaTotalAmt!;
@ -1633,7 +1812,7 @@ setState(() {
gmchasPremiumSummary || gmchasPremiumSummary ||
topUpIsPremiumSummary || topUpIsPremiumSummary ||
topUpParentIsPremiumSummary || topUpParentIsPremiumSummary ||
addOnsDependentIsPremiumSummary)...[ addOnsDependentIsPremiumSummary || opdIsPremiumSummary)...[
Card( Card(
elevation: 0, elevation: 0,
color: Colors.white, color: Colors.white,
@ -1716,7 +1895,7 @@ setState(() {
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'${allTotalSum()}/Year', '${formatAmount(allTotalSum())}/Year',
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: style:
GoogleFonts.poppins( GoogleFonts.poppins(
@ -1936,7 +2115,7 @@ setState(() {
), ),
), ),
Text( Text(
'${addOnsDependentSumInsured != null ? addOnsDependentSumInsured : 'NA'}', '${formatNullableAmount(addOnsDependentSumInsured)}',
textAlign: textAlign:
TextAlign.right, TextAlign.right,
style: GoogleFonts style: GoogleFonts
@ -2193,7 +2372,7 @@ setState(() {
), ),
), ),
Text( Text(
'${topUpSumInsured != null ? topUpSumInsured : 'NA'}', '${formatNullableAmount(topUpSumInsured)}',
textAlign: textAlign:
TextAlign.right, TextAlign.right,
style: GoogleFonts style: GoogleFonts
@ -2271,6 +2450,187 @@ setState(() {
), ),
)), )),
SizedBox(height: showHideTopUpCard == 1 ? 16 : 0), SizedBox(height: showHideTopUpCard == 1 ? 16 : 0),
if (showHideOpdCard == 1)
Card(
elevation: 0,
color: Colors.white,
child: Padding(
padding: Responsive.isDesktop(context)
? EdgeInsets.all(30)
: EdgeInsets.all(10),
child: Column(
children: [
Container(
decoration: BoxDecoration(
color: Color(0xFFFFF1DD),
borderRadius: BorderRadius.circular(5),
),
padding: Responsive.isDesktop(context)
? EdgeInsets.only(
top: 15,
bottom: 15,
left: 25,
right: 25)
: EdgeInsets.only(
top: 10,
bottom: 10,
left: 10,
right: 10),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Expanded(
flex: Responsive.isDesktop(context)
? 9
: 7,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
Responsive.isDesktop(
context)
? opdPolicyName
: opdPolicyType,
textAlign: TextAlign.left,
style:
GoogleFonts.poppins(
fontSize: Responsive
.isDesktop(
context)
? 20
: 14,
fontWeight:
FontWeight.w600,
),
),
if (opdECardDownload != null)
GestureDetector(
onTap: () async {
_launchURL(
opdECardDownload);
},
child: MouseRegion(
cursor:
SystemMouseCursors
.click,
child: Icon(
Icons.file_download,
color: Color(
0xFFE26728),
size: 25,
),
),
),
],
),
],
),
),
Expanded(
flex: Responsive.isDesktop(
context)
? 3
: 5,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
Text(
opdFloaterTextHeading ?? '',
textAlign: TextAlign.right,
style:
GoogleFonts.poppins(
fontSize: Responsive
.isDesktop(
context)
? 18
: 13,
),
),
Text(
'${formatNullableAmount(opdSiValue)}',
textAlign: TextAlign.right,
style:
GoogleFonts.poppins(
fontSize: Responsive
.isDesktop(
context)
? 20
: 14,
fontWeight:
FontWeight.w600,
),
),
],
)),
],
),
),
SizedBox(height: 15),
Container(
padding: Responsive.isDesktop(context)
? EdgeInsets.only(
top: 0,
bottom: 0,
left: 15,
right: 15)
: EdgeInsets.only(
top: 0,
bottom: 0,
left: 5,
right: 5),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children:
(opdMappedFamilyFloatersSi ??
[])
.where((item) =>
item['is_value_exist'] ==
true)
.map<Widget>((item) {
Map<String, dynamic> data =
item['data'];
String formattedDate =
data['dob'] ?? '';
return Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Icon(Icons.arrow_right,
color:
Color(0xFFE26728)),
SizedBox(
width: Responsive.isDesktop(
context)
? 10
: 5),
Expanded(
child: Text(
'${data['name']} ~ ${data['relationship']} ~ DOB : $formattedDate',
style: GoogleFonts.poppins(
fontSize: Responsive
.isDesktop(
context)
? 18
: 14,
color: Color(0xFF232526),
),
),
),
],
);
}).toList(),
),
),
],
),
)),
SizedBox(height: showHideOpdCard == 1 ? 16 : 0),
if (showHideTopUpParentCard == 1) if (showHideTopUpParentCard == 1)
Card( Card(
elevation: 0, elevation: 0,
@ -2460,7 +2820,7 @@ setState(() {
), ),
), ),
Text( Text(
'${topUpParentSiValue != null ? topUpParentSiValue : 'NA'}', '${formatNullableAmount(topUpParentSiValue)}',
textAlign: textAlign:
TextAlign.right, TextAlign.right,
style: GoogleFonts style: GoogleFonts
@ -2552,7 +2912,7 @@ setState(() {
gmchasPremiumSummary || gmchasPremiumSummary ||
topUpIsPremiumSummary || topUpIsPremiumSummary ||
topUpParentIsPremiumSummary || topUpParentIsPremiumSummary ||
addOnsDependentIsPremiumSummary)...[ addOnsDependentIsPremiumSummary || opdIsPremiumSummary)...[
Container( Container(
child: Row( child: Row(
crossAxisAlignment: crossAxisAlignment:
@ -2804,6 +3164,102 @@ setState(() {
), ),
], ],
), ),
if (activeOPDSiData == 1 &&
opdIsPremiumSummary)
TableRow(
children: [
TableCell(
child: Padding(
padding:
EdgeInsets
.all(8.0),
child: Text(
(Responsive.isDesktop(
context)
? (opdPolicyName ??
'Default OPD Name')
: 'GMC - OPD'),
textAlign:
TextAlign
.start,
style: GoogleFonts.poppins(
fontSize:
Responsive.isDesktop(context)
? 14
: 12),
),
),
),
TableCell(
child: Padding(
padding:
EdgeInsets
.all(8.0),
child: Text(
'$opdSiPremiumValue/Year',
textAlign: Responsive
.isDesktop(
context)
? TextAlign
.start
: TextAlign
.start,
style: GoogleFonts.poppins(
fontSize:
Responsive.isDesktop(context)
? 14
: 12),
),
),
),
TableCell(
child: Padding(
padding:
EdgeInsets
.all(8.0),
child: Text(
'$opdSiPremiumGst/-',
textAlign: Responsive
.isDesktop(
context)
? TextAlign
.start
: TextAlign
.start,
style: GoogleFonts.poppins(
fontSize:
Responsive.isDesktop(context)
? 14
: 12),
),
),
),
TableCell(
child: Padding(
padding:
EdgeInsets
.all(8.0),
child: Text(
'$opdSiTotalAmt/Year',
textAlign: Responsive
.isDesktop(
context)
? TextAlign
.start
: TextAlign
.start,
style: GoogleFonts.poppins(
fontSize:
Responsive.isDesktop(context)
? 14
: 12),
),
),
),
],
),
if (activeSiParentData == if (activeSiParentData ==
1 && 1 &&
topUpParentIsPremiumSummary) topUpParentIsPremiumSummary)
@ -3051,7 +3507,7 @@ setState(() {
.end, .end,
children: [ children: [
Text( Text(
'${allTotalSum()}/Year', '${formatAmount(allTotalSum())}/Year',
textAlign: textAlign:
TextAlign.right, TextAlign.right,
style: GoogleFonts style: GoogleFonts
@ -3198,17 +3654,12 @@ setState(() {
child: ElevatedButton( child: ElevatedButton(
onPressed: () async { onPressed: () async {
// Remove siData, siParentData, and dependentData from local storage // Remove siData, siParentData, and dependentData from local storage
final SharedPreferences final SharedPreferences prefs = await SharedPreferences.getInstance();
prefs =
await SharedPreferences
.getInstance();
prefs.remove('siData'); prefs.remove('siData');
prefs prefs.remove('siParentData');
.remove('siParentData'); prefs.remove('dependentData');
prefs.remove( prefs.remove('opdData');
'dependentData'); context.go('/addOnsDetails');
context
.go('/addOnsDetails');
// Navigator.pushNamed( // Navigator.pushNamed(
// context, 'addOnsDetails'); // context, 'addOnsDetails');
}, },
@ -3569,7 +4020,7 @@ setState(() {
), ),
), ),
Text( Text(
'${gpaSumInsured != null ? gpaSumInsured : 'NA'}', '${formatNullableAmount(gpaSumInsured)}',
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: fontSize:
@ -3766,7 +4217,7 @@ setState(() {
gmchasPremiumSummary || gmchasPremiumSummary ||
topUpIsPremiumSummary || topUpIsPremiumSummary ||
topUpParentIsPremiumSummary || topUpParentIsPremiumSummary ||
addOnsDependentIsPremiumSummary) addOnsDependentIsPremiumSummary || opdIsPremiumSummary)
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -3829,6 +4280,15 @@ setState(() {
: value.toStringAsFixed(2); : value.toStringAsFixed(2);
} }
String formatNullableAmount(dynamic value, {String fallback = 'NA'}) {
if (value == null) return fallback;
final numericValue = value is num
? value.toDouble()
: double.tryParse(value.toString());
if (numericValue == null) return value.toString();
return formatAmount(numericValue);
}
List<Widget> generateGmcCards(List<dynamic> data) { List<Widget> generateGmcCards(List<dynamic> data) {
List<Widget> cards = []; List<Widget> cards = [];
// gmcEnrollmentStatus = isEnrollmentOpen(data); // gmcEnrollmentStatus = isEnrollmentOpen(data);
@ -3996,7 +4456,7 @@ setState(() {
), ),
), ),
Text( Text(
'${gmcSumInsured != null ? gmcSumInsured : 'NA'}', '${formatNullableAmount(gmcSumInsured)}',
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: fontSize:
@ -4368,7 +4828,7 @@ setState(() {
child: Padding( child: Padding(
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: Text( child: Text(
'${(policy["si_premium_value"] ?? 0) + (policy["si_gst_value"] ?? 0)}/-', '${(policy["si_premium_value"] ?? 0) + (policy["si_gst_value"] ?? 0)}/Year',
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: Responsive.isDesktop(context) ? 14 : 12), fontSize: Responsive.isDesktop(context) ? 14 : 12),
@ -4444,7 +4904,7 @@ setState(() {
child: Padding( child: Padding(
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: Text( child: Text(
'${(policy["family_floaters_of_dependent_and_si_premium_value"] ?? 0) + (policy["family_floaters_of_dependent_and_gst_value"] ?? 0)}/-', '${(policy["family_floaters_of_dependent_and_si_premium_value"] ?? 0) + (policy["family_floaters_of_dependent_and_gst_value"] ?? 0)}/Year',
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: Responsive.isDesktop(context) ? 14 : 12), fontSize: Responsive.isDesktop(context) ? 14 : 12),

View File

@ -0,0 +1,7 @@
import 'browser_href_stub.dart'
if (dart.library.html) 'browser_href_web.dart' as impl;
String currentBrowserHref() => impl.currentBrowserHref();
void stripPayloadParamFromBrowserUrl() =>
impl.stripPayloadParamFromBrowserUrl();

View File

@ -0,0 +1,3 @@
String currentBrowserHref() => '';
void stripPayloadParamFromBrowserUrl() {}

View File

@ -0,0 +1,22 @@
import 'dart:html' as html;
String currentBrowserHref() => html.window.location.href;
/// Removes `payload` from the query string so a failed SSO return does not
/// re-trigger handling when [GoRouter] rebuilds `/login`.
void stripPayloadParamFromBrowserUrl() {
final u = Uri.parse(html.window.location.href);
if (!u.queryParameters.containsKey('payload')) return;
final m = Map<String, String>.from(u.queryParameters);
m.remove('payload');
final clean = Uri(
scheme: u.scheme,
userInfo: u.userInfo.isEmpty ? null : u.userInfo,
host: u.host.isEmpty ? null : u.host,
port: u.hasPort ? u.port : null,
path: u.path,
queryParameters: m.isEmpty ? null : m,
fragment: u.fragment,
);
html.window.history.replaceState(null, '', clean.toString());
}

View File

@ -0,0 +1,4 @@
import 'sso_same_tab_redirect_stub.dart'
if (dart.library.html) 'sso_same_tab_redirect_web.dart' as impl;
void ssoRedirectSameTab(String url) => impl.ssoRedirectSameTab(url);

View File

@ -0,0 +1,2 @@
/// Non-web: caller should not invoke same-tab redirect.
void ssoRedirectSameTab(String url) {}

View File

@ -0,0 +1,6 @@
import 'dart:html' as html;
/// Full-page navigation so Microsoft IdP and SAML complete in the same tab.
void ssoRedirectSameTab(String url) {
html.window.location.assign(url);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,274 @@
import 'dart:convert';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:jwt_decode/jwt_decode.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../config/environment.dart';
import '../customAppBar/toastHelper.dart';
import '../logger.dart';
import 'service/SessionManager.dart';
import 'service/TokenService.dart';
/// Where the IdP should return the browser after SAML (web).
///
/// Uses **`/login`** (not `/sso-login`) so static hosts that do not rewrite
/// unknown paths to `index.html` still return **200** and Flutter can read
/// `?payload=`. `/sso-login` is still supported via `web/index.html` redirect.
String buildWebSsoRelayStateUrl() {
if (!kIsWeb) return '';
final origin = Uri.base.origin;
final frag = Uri.base.fragment;
// Hash routing: e.g. `http://host/#/login` return with payload on `#/login`
if (frag.startsWith('/')) {
return '$origin/#/login';
}
final href = Environment.baseHref;
if (href.isEmpty || href == '/') {
return '$origin/login';
}
final trimmed =
href.endsWith('/') ? href.substring(0, href.length - 1) : href;
return '$origin$trimmed/login';
}
/// Decodes the `payload` query value (base64 / base64url) into a JSON map.
Map<String, dynamic> decodeSsoPayloadQueryValue(String encoded) {
var normalized = encoded.trim().replaceAll('-', '+').replaceAll('_', '/');
final mod = normalized.length % 4;
if (mod > 0) {
normalized = normalized.padRight(normalized.length + (4 - mod), '=');
}
final jsonStr = utf8.decode(base64.decode(normalized));
final decoded = json.decode(jsonStr);
if (decoded is Map<String, dynamic>) return decoded;
if (decoded is Map) {
return Map<String, dynamic>.from(decoded);
}
throw const FormatException('SSO payload is not a JSON object');
}
String? readPayloadFromRouterUri(Uri uri) {
final direct = uri.queryParameters['payload'];
if (direct != null && direct.isNotEmpty) return direct;
final frag = uri.fragment;
if (frag.contains('?')) {
final queryPart = frag.split('?').last;
return Uri.splitQueryString(queryPart)['payload'];
}
return null;
}
/// Locates the raw `payload` query value on a SAML return URL, e.g.
/// `https://staging.nhanceindia.in/sso-login?payload=eyJ...` or
/// `https://host/#/sso-login?payload=eyJ...`.
String? readSsoPayloadEncodedFromReturnUrl(String url) {
final trimmed = url.trim();
if (trimmed.isEmpty) return null;
final uri = Uri.tryParse(trimmed);
if (uri != null) {
final fromUri = readPayloadFromRouterUri(uri);
if (fromUri != null && fromUri.isNotEmpty) {
return fromUri;
}
}
// Fallback: stop before `#` so `...?payload=eyJ...#/login` does not swallow the hash.
final ampOrQ = RegExp(r'(?:\?|&)payload=([^&#]+)');
final match = ampOrQ.firstMatch(trimmed);
if (match != null) {
final raw = match.group(1);
if (raw != null && raw.isNotEmpty) {
return Uri.decodeQueryComponent(raw);
}
}
final hashIdx = trimmed.indexOf('#');
if (hashIdx != -1) {
final afterHash = trimmed.substring(hashIdx + 1);
if (afterHash.contains('payload=')) {
final queryPart =
afterHash.contains('?') ? afterHash.split('?').last : afterHash;
final fromFrag = Uri.splitQueryString(queryPart)['payload'];
if (fromFrag != null && fromFrag.isNotEmpty) {
return fromFrag;
}
}
}
return null;
}
Map<String, dynamic>? decodeSsoPayloadFromUrl(String url) {
try {
final raw = readSsoPayloadEncodedFromReturnUrl(url);
if (raw == null || raw.isEmpty) return null;
return decodeSsoPayloadQueryValue(raw);
} catch (_) {
return null;
}
}
/// JWT claim maps for pre-enrollment (`data`) and post-enrollment tokens when
/// those fields hold a JWT string (typically after successful SAML).
class SsoPayloadJwtDecode {
final Map<String, dynamic>? preTokenClaims;
final Map<String, dynamic>? postTokenClaims;
const SsoPayloadJwtDecode({
this.preTokenClaims,
this.postTokenClaims,
});
}
SsoPayloadJwtDecode decodeJwtClaimsFromSsoPayloadMap(
Map<String, dynamic> data) {
Map<String, dynamic>? preClaims;
Map<String, dynamic>? postClaims;
final pre = data['data'];
if (pre is String && pre.contains('.')) {
try {
preClaims = Jwt.parseJwt(pre);
} catch (_) {}
}
final rawPe = data['post_enrollment'];
if (rawPe is Map) {
final pt = rawPe['data'];
if (pt is String && pt.contains('.')) {
try {
postClaims = Jwt.parseJwt(pt);
} catch (_) {}
}
}
return SsoPayloadJwtDecode(
preTokenClaims: preClaims,
postTokenClaims: postClaims,
);
}
String prettySsoJsonForConsole(Object? value) {
if (value == null) return 'null';
try {
return const JsonEncoder.withIndent(' ').convert(value);
} catch (_) {
return value.toString();
}
}
/// Writes JWT claim maps to stdout / the browser DevTools console. Unlike
/// [logDebug], this is not filtered by [LoggerConfig.minLevel].
void printSsoDecodedTokensToConsole(SsoPayloadJwtDecode jwt) {
print(
'[SSO] decodedToken (pre / data field):\n${prettySsoJsonForConsole(jwt.preTokenClaims)}');
print(
'[SSO] decodedToken (post_enrollment.data):\n${prettySsoJsonForConsole(jwt.postTokenClaims)}');
}
Future<void> _persistClientBranding() async {
final session = SessionManager();
final pre = await TokenService.getPreToken();
if (pre == null || pre.isEmpty) return;
final url = Uri.parse(Environment.apiUrlEnrollment +
'getClientDetails?post_client_id=${session.client_id}&post_branch_id=${session.empClientBranchId}&pre_client_id=${session.enrollmentClient_id}&pre_branch_id=${session.enrollmentEmpClientBranchId}');
try {
final response = await http.get(
url,
headers: {
'Authorization': 'Bearer $pre',
'APP-SIGNATURE':
'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
},
);
if (response.statusCode != 200) return;
final data = json.decode(response.body) as Map<String, dynamic>;
if (!data.containsKey('data')) return;
final clientDetails = data['data'];
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
'clientLogo', clientDetails['client']['client_logo'] as String? ?? '');
await prefs.setString(
'clientName', clientDetails['client']['client_name'] as String? ?? '');
await prefs.setString('addon_subheading',
clientDetails['client']['addon_subheading'] as String? ?? '');
} catch (e) {
logDebug('SSO client branding fetch failed: $e');
}
}
/// Completes login after SAML callback payload has been decoded to a map.
Future<void> completeLoginFromSsoPayloadMap(
BuildContext context, Map<String, dynamic> data) async {
logDebug('SSO payload map: $data');
print('[SSO] payload map (decoded from URL):\n${prettySsoJsonForConsole(data)}');
final jwt = decodeJwtClaimsFromSsoPayloadMap(data);
printSsoDecodedTokensToConsole(jwt);
if (jwt.preTokenClaims != null) {
logDebug('SSO decodedToken (pre / data): ${jwt.preTokenClaims}');
}
if (jwt.postTokenClaims != null) {
logDebug('SSO decodedToken (post_enrollment / data): ${jwt.postTokenClaims}');
}
if (data['status']?.toString() != 'success') {
final msg = data['message']?.toString() ?? 'SSO sign-in was not successful';
if (context.mounted) ToastHelper.showErrorToast(context, msg);
if (context.mounted) context.go('/login');
return;
}
String? preToken;
if (data['data'] is String) {
preToken = data['data'] as String;
}
String? postToken;
Map<String, dynamic>? postEnrollment;
final rawPe = data['post_enrollment'];
if (rawPe is Map) {
postEnrollment = Map<String, dynamic>.from(rawPe);
}
if (postEnrollment != null && postEnrollment['data'] is String) {
postToken = postEnrollment['data'] as String;
}
await TokenService.saveTokens(preToken: preToken, postToken: postToken);
if (postEnrollment != null &&
postEnrollment['status']?.toString() == 'success' &&
postToken != null &&
postToken.isNotEmpty) {
await SessionManager().initializeFromPostToken(postToken);
}
if (data['status']?.toString() == 'success' &&
preToken != null &&
preToken.isNotEmpty) {
await SessionManager().initializeFromPreToken(preToken);
}
await _persistClientBranding();
if (!context.mounted) return;
if (postToken != null && postToken.isNotEmpty) {
ToastHelper.showSuccessToast(context, 'Successfully Logged In');
SessionManager().unlockMobileAppSession();
context.go('/home');
} else {
SessionManager().unlockMobileAppSession();
context.go('/empDetails');
}
}
Future<void> completeLoginFromSsoPayloadString(
BuildContext context, String encodedPayload) async {
final data = decodeSsoPayloadQueryValue(encodedPayload);
await completeLoginFromSsoPayloadMap(context, data);
}

View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'login_saml_auth.dart';
/// Mobile: in-app WebView for SAML entry; when URL contains `payload=`, decodes and pops.
class LoginSamlWebViewScreen extends StatefulWidget {
final String initialUrl;
const LoginSamlWebViewScreen({super.key, required this.initialUrl});
@override
State<LoginSamlWebViewScreen> createState() => _LoginSamlWebViewScreenState();
}
class _LoginSamlWebViewScreenState extends State<LoginSamlWebViewScreen> {
late final WebViewController _controller;
bool _finished = false;
void _tryFinishWithUrl(String url) {
if (_finished || !mounted) return;
if (!url.contains('payload=')) return;
final decoded = decodeSsoPayloadFromUrl(url);
if (decoded != null) {
_finished = true;
Navigator.of(context).pop(decoded);
}
}
void _tryFinishFromHttpError(HttpResponseError error) {
if (_finished || !mounted) return;
final status = error.response?.statusCode;
if (status == null) return;
if (status < 400) return;
final reqUri = error.request?.uri;
final resUri = error.response?.uri;
if (reqUri != null) {
_tryFinishWithUrl(reqUri.toString());
}
if (!_finished && resUri != null) {
_tryFinishWithUrl(resUri.toString());
}
}
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (!_finished && request.url.contains('payload=')) {
_tryFinishWithUrl(request.url);
if (_finished) return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onPageStarted: (String url) => _tryFinishWithUrl(url),
onPageFinished: (String url) => _tryFinishWithUrl(url),
onUrlChange: (UrlChange change) {
final url = change.url;
if (url != null && url.isNotEmpty) {
_tryFinishWithUrl(url);
}
},
onHttpError: _tryFinishFromHttpError,
onWebResourceError: (WebResourceError error) {
final url = error.url;
if (url != null && url.isNotEmpty) {
_tryFinishWithUrl(url);
}
},
),
)
..loadRequest(Uri.parse(widget.initialUrl));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sign in with SSO')),
body: WebViewWidget(controller: _controller),
);
}
}

View File

@ -1043,7 +1043,7 @@ class _claimsState extends State<claims> {
Text( Text(
isRetail ? insurerShortName : policyName, isRetail ? insurerShortName : policyName,
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: Responsive.isDesktop(context) ? 18 : 14, fontSize: Responsive.isDesktop(context) ? 18 : 11,
fontWeight: FontWeight.w600), fontWeight: FontWeight.w600),
), ),
], ],
@ -1065,7 +1065,7 @@ class _claimsState extends State<claims> {
Text( Text(
isRetail ? policyType : policyStatus, isRetail ? policyType : policyStatus,
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: Responsive.isDesktop(context) ? 18 : 14, fontSize: Responsive.isDesktop(context) ? 18 : 11,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: isRetail color: isRetail
? Colors.black ? Colors.black
@ -1093,7 +1093,7 @@ class _claimsState extends State<claims> {
Text( Text(
isRetail ? vehicleNo : "$siValue", isRetail ? vehicleNo : "$siValue",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: Responsive.isDesktop(context) ? 18 : 14, fontSize: Responsive.isDesktop(context) ? 18 : 11,
fontWeight: FontWeight.w600), fontWeight: FontWeight.w600),
), ),
], ],

View File

@ -222,7 +222,7 @@ class _helpState extends State<help> {
// trackClaimsClosedList = []; // trackClaimsClosedList = [];
// }); // });
// } else { // } else {
// ToastHelper.showErrorToast(context, 'Something went wrong'); // ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
// logDebug('API request failed with status: ${response['status']}'); // logDebug('API request failed with status: ${response['status']}');
// } // }
} }
@ -266,7 +266,7 @@ class _helpState extends State<help> {
// setState(() { // setState(() {
// isLoading = false; // isLoading = false;
// }); // });
// ToastHelper.showErrorToast(context, 'Something went wrong'); // ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
// logDebug('API request failed with status: ${response['status']}'); // logDebug('API request failed with status: ${response['status']}');
// } // }
} catch (e) { } catch (e) {

View File

@ -236,6 +236,7 @@ class _planclaimsformState extends State<planclaimsform> {
return { return {
'id': int.parse(memberList['id']), 'id': int.parse(memberList['id']),
'name': memberList['name'], 'name': memberList['name'],
'relationship': memberList['relationship'],
}; };
}).toList(); }).toList();
logDebug('employeePolicyList : $employeePolicyList'); logDebug('employeePolicyList : $employeePolicyList');
@ -566,6 +567,7 @@ class _planclaimsformState extends State<planclaimsform> {
return { return {
'id': int.parse(memberList['id']), 'id': int.parse(memberList['id']),
'name': memberList['name'], 'name': memberList['name'],
'relationship': memberList['relationship'],
}; };
}).toList(); }).toList();
logDebug('employeePolicyList : $employeePolicyList'); logDebug('employeePolicyList : $employeePolicyList');
@ -753,59 +755,59 @@ class _planclaimsformState extends State<planclaimsform> {
} }
// NEW VALIDATION: At least one PDF must be uploaded // NEW VALIDATION: At least one PDF must be uploaded
// bool hasPdf = uploadedFiles.any((uf) { bool hasPdf = uploadedFiles.any((uf) {
// final ext = uf.file.extension?.toLowerCase() ?? ''; final ext = uf.file.extension?.toLowerCase() ?? '';
// return ext == 'pdf'; return ext == 'pdf';
// }); });
//
// if (!hasPdf) { if (!hasPdf) {
// ToastHelper.showErrorToast(context, 'Please upload at least one PDF document'); ToastHelper.showErrorToast(context, 'Please upload at least one PDF document');
// setState(() => isLoading = false); setState(() => isLoading = false);
// return; return;
// } }
// Add files // Add files
// for (var uf in uploadedFiles) {
// final pf = uf.file;
// if (pf.bytes != null) {
// request.files.add(http.MultipartFile.fromBytes(
// 'claim_docs[]',
// pf.bytes!,
// filename: pf.name,
// ));
// }
// }
// Add files convert images to PDF if needed
for (var uf in uploadedFiles) { for (var uf in uploadedFiles) {
final pf = uf.file; final pf = uf.file;
final ext = pf.extension?.toLowerCase() ?? '';
if (pf.bytes != null) { if (pf.bytes != null) {
Uint8List fileBytes = pf.bytes!; request.files.add(http.MultipartFile.fromBytes(
'claim_docs[]',
if (['jpg', 'jpeg', 'png', 'heic'].contains(ext)) { pf.bytes!,
// Convert image PDF filename: pf.name,
final pdf = pw.Document(); ));
final image = pw.MemoryImage(fileBytes);
pdf.addPage(pw.Page(
build: (pw.Context context) =>
pw.Center(child: pw.Image(image, fit: pw.BoxFit.contain)),
));
fileBytes = await pdf.save();
final pdfFileName = pf.name.replaceAll(RegExp(r'\.\w+$'), '.pdf');
logDebug('📄 Converted image ${pf.name} → PDF ($pdfFileName)');
request.files.add(http.MultipartFile.fromBytes(
'claim_docs[]', fileBytes, filename: pdfFileName));
} else {
// Already a PDF
request.files.add(http.MultipartFile.fromBytes(
'claim_docs[]', fileBytes, filename: pf.name));
}
} }
} }
// Add files convert images to PDF if needed
// for (var uf in uploadedFiles) {
// final pf = uf.file;
// final ext = pf.extension?.toLowerCase() ?? '';
// if (pf.bytes != null) {
// Uint8List fileBytes = pf.bytes!;
// if (['jpg', 'jpeg', 'png', 'heic'].contains(ext)) {
// // Convert image PDF
// final pdf = pw.Document();
// final image = pw.MemoryImage(fileBytes);
// pdf.addPage(pw.Page(
// build: (pw.Context context) =>
// pw.Center(child: pw.Image(image, fit: pw.BoxFit.contain)),
// ));
// fileBytes = await pdf.save();
// final pdfFileName = pf.name.replaceAll(RegExp(r'\.\w+$'), '.pdf');
// logDebug('📄 Converted image ${pf.name} → PDF ($pdfFileName)');
// request.files.add(http.MultipartFile.fromBytes(
// 'claim_docs[]', fileBytes, filename: pdfFileName));
// } else {
// // Already a PDF
// request.files.add(http.MultipartFile.fromBytes(
// 'claim_docs[]', fileBytes, filename: pf.name));
// }
// }
// }
// Combine all names into a JSON array string // Combine all names into a JSON array string
final claimDocNames = uploadedFiles.map((uf) => uf.label.trim()).toList(); final claimDocNames = uploadedFiles.map((uf) => uf.label.trim()).toList();

View File

@ -141,7 +141,7 @@ class _raisedTicketHistoryState extends State<raisedTicketHistory> {
// setState(() { // setState(() {
// isLoading = false; // isLoading = false;
// }); // });
// ToastHelper.showErrorToast(context, 'Something went wrong'); // ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
// logDebug('API request failed with status: ${response['status']}'); // logDebug('API request failed with status: ${response['status']}');
// } // }
} catch (e) { } catch (e) {

View File

@ -193,7 +193,7 @@ class _retailClaimFormsState extends State<retailClaimForm> {
} }
} catch (e) { } catch (e) {
logDebug("Retail claim submit ERROR: $e"); logDebug("Retail claim submit ERROR: $e");
ToastHelper.showErrorToast(context, "Something went wrong"); ToastHelper.showErrorToast(context, "Unable to process. Please try again later");
} }
setState(() => isLoading = false); setState(() => isLoading = false);

View File

@ -212,7 +212,7 @@ class _tickettracklistState extends State<tickettracklist> {
// setState(() { // setState(() {
// isLoading = false; // isLoading = false;
// }); // });
// ToastHelper.showErrorToast(context, 'Something went wrong'); // ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
// logDebug('API request failed with status: ${response['status']}'); // logDebug('API request failed with status: ${response['status']}');
// } // }
} catch (e) { } catch (e) {

View File

@ -156,6 +156,22 @@ class SessionManager {
bool isReady = false; bool isReady = false;
/// Mobile only: false after a full app restart until login / MPIN succeeds.
/// Stays true while the process stays alive (e.g. user only minimized the app).
/// Web always behaves as unlocked ([isMobileAppSessionUnlocked] is true).
bool _mobileAppSessionUnlocked = false;
bool get isMobileAppSessionUnlocked =>
kIsWeb ? true : _mobileAppSessionUnlocked;
void unlockMobileAppSession() {
if (!kIsWeb) _mobileAppSessionUnlocked = true;
}
void lockMobileAppSession() {
if (!kIsWeb) _mobileAppSessionUnlocked = false;
}
// ===== Post Enrollment Fields ===== // ===== Post Enrollment Fields =====
String? mobileNo; String? mobileNo;
String? empClientBranchId; String? empClientBranchId;
@ -279,5 +295,6 @@ class SessionManager {
await prefs.clear(); await prefs.clear();
await DataManager().clearData(); await DataManager().clearData();
await TokenService.clearTokens(); await TokenService.clearTokens();
lockMobileAppSession();
} }
} }

View File

@ -120,12 +120,12 @@ import 'package:nhance_app_pwa/logger.dart';
} }
} }
} else { } else {
logDebug('checkLoginPin Something went wrong'); logDebug('checkLoginPin Unable to process. Please try again later');
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
throw Exception('Failed to verify pin number'); throw Exception('Failed to verify pin number');
} }
} catch (e) { } catch (e) {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
logDebug('Error: $e'); logDebug('Error: $e');
} }
} }
@ -233,7 +233,7 @@ import 'package:nhance_app_pwa/logger.dart';
throw Exception('Failed to load data'); throw Exception('Failed to load data');
} }
} catch (e) { } catch (e) {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
context.go('/login'); context.go('/login');
logDebug('Error: $e'); logDebug('Error: $e');
} }
@ -309,6 +309,7 @@ import 'package:nhance_app_pwa/logger.dart';
logDebug('Token12345: ${await TokenService.getPostToken()}'); logDebug('Token12345: ${await TokenService.getPostToken()}');
logDebug('Token00000'); logDebug('Token00000');
// if (context.mounted) { // if (context.mounted) {
SessionManager().unlockMobileAppSession();
context.go('/home'); context.go('/home');
// } // }
// Navigator.pushReplacementNamed(context, 'home'); // Navigator.pushReplacementNamed(context, 'home');
@ -353,6 +354,7 @@ import 'package:nhance_app_pwa/logger.dart';
// enrollmentEmp_status == 'active') { // enrollmentEmp_status == 'active') {
// Navigator.pushReplacementNamed(context, 'home'); // Navigator.pushReplacementNamed(context, 'home');
// } else { // } else {
SessionManager().unlockMobileAppSession();
context.go('/empDetails'); context.go('/empDetails');
// Navigator.pushReplacementNamed(context, 'empDetails'); // Navigator.pushReplacementNamed(context, 'empDetails');
// } // }
@ -370,7 +372,7 @@ import 'package:nhance_app_pwa/logger.dart';
try { try {
await enterPinApi(_pinController.text); await enterPinApi(_pinController.text);
} catch (e) { } catch (e) {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
logDebug('Error: $e'); logDebug('Error: $e');
} finally { } finally {
if (mounted) { if (mounted) {
@ -469,7 +471,7 @@ import 'package:nhance_app_pwa/logger.dart';
throw Exception('Failed to load data'); throw Exception('Failed to load data');
} }
} catch (e) { } catch (e) {
// ToastHelper.showErrorToast(context, 'Something went wrong'); // ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
logDebug('Error: $e'); logDebug('Error: $e');
} finally { } finally {
if (mounted) { if (mounted) {

View File

@ -135,7 +135,7 @@ class _changePinState extends State<changePin> {
} }
} }
} catch (e) { } catch (e) {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
logDebug('Error: $e'); logDebug('Error: $e');
} finally { } finally {
if (mounted) { if (mounted) {

View File

@ -143,9 +143,11 @@ class _pinSettingPageState extends State<pinSettingPage> {
await _authService.authenticateWithBiometrics(); await _authService.authenticateWithBiometrics();
if (biometricEnabled) { if (biometricEnabled) {
if (_postToken != null && _postToken.isNotEmpty) { if (_postToken != null && _postToken.isNotEmpty) {
SessionManager().unlockMobileAppSession();
context.go('/home'); context.go('/home');
// Navigator.pushReplacementNamed(context, 'home'); // Navigator.pushReplacementNamed(context, 'home');
} else { } else {
SessionManager().unlockMobileAppSession();
context.go('/empDetails'); context.go('/empDetails');
// Navigator.pushReplacementNamed(context, 'empDetails'); // Navigator.pushReplacementNamed(context, 'empDetails');
} }
@ -158,9 +160,11 @@ class _pinSettingPageState extends State<pinSettingPage> {
} }
} else { } else {
if (_postToken != null && _postToken.isNotEmpty) { if (_postToken != null && _postToken.isNotEmpty) {
SessionManager().unlockMobileAppSession();
context.go('/home'); context.go('/home');
// Navigator.pushReplacementNamed(context, 'home'); // Navigator.pushReplacementNamed(context, 'home');
} else { } else {
SessionManager().unlockMobileAppSession();
context.go('/empDetails'); context.go('/empDetails');
// Navigator.pushReplacementNamed(context, 'empDetails'); // Navigator.pushReplacementNamed(context, 'empDetails');
} }
@ -194,7 +198,7 @@ class _pinSettingPageState extends State<pinSettingPage> {
} }
// } // }
} catch (e) { } catch (e) {
ToastHelper.showErrorToast(context, 'Something went wrong'); ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
print('Error: $e'); print('Error: $e');
} finally { } finally {
if (mounted) { if (mounted) {
@ -237,9 +241,11 @@ class _pinSettingPageState extends State<pinSettingPage> {
await _authService.saveSkipStatus(0); await _authService.saveSkipStatus(0);
if (_postToken != null && _postToken.isNotEmpty) { if (_postToken != null && _postToken.isNotEmpty) {
SessionManager().unlockMobileAppSession();
context.go('/home'); context.go('/home');
// Navigator.pushReplacementNamed(context, 'home'); // Navigator.pushReplacementNamed(context, 'home');
} else { } else {
SessionManager().unlockMobileAppSession();
context.go('/empDetails'); context.go('/empDetails');
// Navigator.pushReplacementNamed(context, 'empDetails'); // Navigator.pushReplacementNamed(context, 'empDetails');
} }

View File

@ -163,7 +163,7 @@ class _setPasswordState extends State<setPassword> {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
// ToastHelper.showErrorToast(context, 'Something went wrong'); // ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
logDebug('Error: $e'); logDebug('Error: $e');
} }

View File

@ -0,0 +1,62 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../customAppBar/toastHelper.dart';
import '../logger.dart';
import 'helpers/browser_href.dart';
import 'login_saml_auth.dart';
/// Handles the browser return URL after SAML (query or hash `payload=`).
class SsoLoginReturnPage extends StatefulWidget {
const SsoLoginReturnPage({super.key});
@override
State<SsoLoginReturnPage> createState() => _SsoLoginReturnPageState();
}
class _SsoLoginReturnPageState extends State<SsoLoginReturnPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _consumePayload());
}
Future<void> _consumePayload() async {
if (!mounted) return;
final routerUri = GoRouterState.of(context).uri;
final href = kIsWeb ? currentBrowserHref() : routerUri.toString();
Map<String, dynamic>? decoded;
for (final candidate in <String>[
href,
if (kIsWeb) Uri.base.toString(),
routerUri.toString(),
]) {
if (candidate.isEmpty) continue;
decoded = decodeSsoPayloadFromUrl(candidate);
if (decoded != null) break;
}
if (!mounted) return;
if (decoded == null) {
logDebug('SSO return: no payload in href=$href router=${routerUri.toString()}');
ToastHelper.showWarningToast(
context, 'SSO return missing or invalid payload');
context.go('/login');
return;
}
logDebug('SSO return: decoded payload (keys)=${decoded.keys.toList()}');
await completeLoginFromSsoPayloadMap(context, decoded);
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
}

View File

@ -313,7 +313,7 @@ import 'package:nhance_app_pwa/logger.dart';
// }); // });
// final SharedPreferences prefs = await SharedPreferences.getInstance(); // final SharedPreferences prefs = await SharedPreferences.getInstance();
// prefs.clear(); // prefs.clear();
// ToastHelper.showWarningToast(context, 'Something went wrong'); // ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
// throw Exception('Failed to verify OTP'); // throw Exception('Failed to verify OTP');
// } // }
// } catch (e) { // } catch (e) {
@ -323,7 +323,7 @@ import 'package:nhance_app_pwa/logger.dart';
// logDebug('Error: $e'); // logDebug('Error: $e');
// final SharedPreferences prefs = await SharedPreferences.getInstance(); // final SharedPreferences prefs = await SharedPreferences.getInstance();
// prefs.clear(); // prefs.clear();
// ToastHelper.showWarningToast(context, 'Something went wrong'); // ToastHelper.showWarningToast(context, 'Unable to process. Please try again later');
// // Show a Snackbar if there's an error while verifying OTP // // Show a Snackbar if there's an error while verifying OTP
// logDebug('Failed to verify OTP. Please try again.'); // logDebug('Failed to verify OTP. Please try again.');
// } // }
@ -674,12 +674,12 @@ import 'package:nhance_app_pwa/logger.dart';
// // Navigator.pushReplacementNamed(context, 'pinSettingPage'); // // Navigator.pushReplacementNamed(context, 'pinSettingPage');
// } // }
// } else { // } else {
// logDebug('checkLoginPin Something went wrong'); // logDebug('checkLoginPin Unable to process. Please try again later');
// ToastHelper.showErrorToast(context, 'Something went wrong'); // ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
// throw Exception('Failed to verify pin number'); // throw Exception('Failed to verify pin number');
// } // }
// } catch (e) { // } catch (e) {
// ToastHelper.showErrorToast(context, 'Something went wrong'); // ToastHelper.showErrorToast(context, 'Unable to process. Please try again later');
// logDebug('Error: $e'); // logDebug('Error: $e');
// } // }
// } // }

View File

@ -17,8 +17,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
#version: 1.2.41+98 #version: 1.2.41+98
version: 1.0.32+38 version: 1.0.35+41
#version: 2.0.20+58 #version: 2.0.23+61
environment: environment:
sdk: '>=3.3.3 <4.0.0' sdk: '>=3.3.3 <4.0.0'
@ -101,6 +101,7 @@ flutter:
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
assets: assets:
- assets/ - assets/
- assets/images/login/
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg

View File

@ -4,6 +4,22 @@
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF">
<!-- If the IdP redirects to /sso-login?payload=…, many CDNs return 404 (no SPA
fallback). Send the browser to /login?payload=… so index.html loads. -->
<script>
(function () {
try {
var path = window.location.pathname || '';
var search = window.location.search || '';
if (!search || search.indexOf('payload=') === -1) return;
if (!/\/sso-login\/?$/.test(path)) return;
var origin = window.location.origin;
var prefix = path.replace(/\/sso-login\/?$/, '') || '';
window.location.replace(origin + prefix + '/login' + search);
} catch (e) {}
})();
</script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="Nhance Benefits"> <meta name="description" content="Nhance Benefits">
@ -47,6 +63,9 @@
<script src="flutter.js" defer></script> <script src="flutter.js" defer></script>
</head> </head>
<body style="overflow:hidden"> <body style="overflow:hidden">
<script>
try { console.log('[SSO] page URL (before Flutter):', window.location.href); } catch (e) {}
</script>
<div id="loading_indicator" class="container overlay"> <div id="loading_indicator" class="container overlay">
<img class="indicator" src="assets/nhance-loader.gif" alt=""> <img class="indicator" src="assets/nhance-loader.gif" alt="">
</div> </div>