Smal Flow
This commit is contained in:
parent
1288a67256
commit
f4e28d2629
@ -19,12 +19,12 @@ if (project.hasProperty('google-services.json')) {
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '61'
|
||||
flutterVersionCode = '63'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '2.0.23'
|
||||
flutterVersionName = '2.0.25'
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
|
||||
@ -4,6 +4,9 @@ enum Flavor { dev, uat, prod, prod1 }
|
||||
class Environment {
|
||||
static Flavor flavor = Flavor.dev; // overwritten by each main_*.dart
|
||||
|
||||
/// Chatbot FAB + window. Set to `true` when ready to enable.
|
||||
static const bool chatbotEnabled = false;
|
||||
|
||||
static bool get isProd => flavor == Flavor.prod || flavor == Flavor.prod1;
|
||||
|
||||
static String get apiUrl {
|
||||
@ -51,12 +54,11 @@ class Environment {
|
||||
case Flavor.uat:
|
||||
return "https://appstage.nhanceindia.in/apex/saml/login";
|
||||
case Flavor.prod:
|
||||
return "https://app.nhanceindia.in/saml/login";
|
||||
return "https://app.nhanceindia.in/enrolment/saml/login";
|
||||
case Flavor.prod1:
|
||||
return "https://app.nhanceindia.in/saml/login";
|
||||
return "https://app.nhanceindia.in/enrolment/saml/login";
|
||||
}
|
||||
}
|
||||
|
||||
static String get baseHref {
|
||||
switch (flavor) {
|
||||
case Flavor.dev:
|
||||
|
||||
@ -11,6 +11,11 @@ import '../pages/service/SessionManager.dart';
|
||||
import '../pages/service/TokenService.dart';
|
||||
import '../pages/service/popup_helper.dart';
|
||||
class CustomAppBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
/// When true, shows only the client logo (no nav menu).
|
||||
final bool logoOnly;
|
||||
|
||||
const CustomAppBar({super.key, this.logoOnly = false});
|
||||
|
||||
@override
|
||||
_CustomAppBarState createState() => _CustomAppBarState();
|
||||
|
||||
@ -102,7 +107,7 @@ class _CustomAppBarState extends State<CustomAppBar> {
|
||||
// Expanded(
|
||||
// // flex: Responsive.isDesktop(context) ? 1 : 9,
|
||||
// child:
|
||||
if(Responsive.isDesktop(context))
|
||||
if (!widget.logoOnly && Responsive.isDesktop(context))
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
22
lib/customAppBar/nhance_loading_screen.dart
Normal file
22
lib/customAppBar/nhance_loading_screen.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Full-screen Nhance loader (same style as splash / home overlays).
|
||||
class NhanceLoadingScreen extends StatelessWidget {
|
||||
const NhanceLoadingScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: const Color(0x98FFFCE5),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/nhance-loader.gif',
|
||||
height: 60,
|
||||
width: 60,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:nhance_app_pwa/config/environment.dart';
|
||||
import 'package:nhance_app_pwa/customAppBar/responsive.dart';
|
||||
import 'package:nhance_app_pwa/features/chatbot/application/chatbot_controller.dart';
|
||||
import 'package:nhance_app_pwa/features/chatbot/presentation/widgets/chatbot_fab.dart';
|
||||
@ -36,6 +37,10 @@ class _ChatbotHostState extends ConsumerState<ChatbotHost> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!Environment.chatbotEnabled) {
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
final state = ref.watch(chatbotControllerProvider);
|
||||
final controller = ref.read(chatbotControllerProvider.notifier);
|
||||
|
||||
|
||||
@ -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/empReview.dart';
|
||||
import 'package:nhance_app_pwa/pages/login.dart';
|
||||
import 'package:nhance_app_pwa/pages/login_saml_auth.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/claimprocess.dart';
|
||||
@ -47,6 +48,7 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'config/environment.dart';
|
||||
import 'logger.dart';
|
||||
import 'router/app_router.dart';
|
||||
// import 'dart:html' as html;
|
||||
|
||||
Future<void> main() async {
|
||||
@ -97,6 +99,10 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
Future<String?> tokenRedirectLogic(
|
||||
BuildContext context, GoRouterState state) async {
|
||||
logDebug('ABCDEFGH');
|
||||
|
||||
final ssoLoginRedirect = redirectLoginWithSsoPayload(state);
|
||||
if (ssoLoginRedirect != null) return ssoLoginRedirect;
|
||||
|
||||
const guestRoutes = [
|
||||
'/login',
|
||||
'/sso-login',
|
||||
@ -104,6 +110,8 @@ Future<String?> tokenRedirectLogic(
|
||||
'/mailVerify',
|
||||
'/pinPage',
|
||||
'/splash',
|
||||
'/privacypolicy',
|
||||
'/termsofuse',
|
||||
];
|
||||
|
||||
final hasToken = await TokenService.hasValidToken();
|
||||
@ -116,6 +124,10 @@ Future<String?> tokenRedirectLogic(
|
||||
|
||||
if (hasToken) {
|
||||
await session.restoreSession();
|
||||
final destination = await TokenService.defaultRouteForStoredTokens();
|
||||
if (location == '/home' && destination == '/empDetails') {
|
||||
return '/empDetails';
|
||||
}
|
||||
}
|
||||
|
||||
if (!kIsWeb && hasToken) {
|
||||
@ -123,12 +135,12 @@ Future<String?> tokenRedirectLogic(
|
||||
if (reauth != null) return reauth;
|
||||
}
|
||||
|
||||
// Logged in on a guest route → home (only when mobile session is unlocked).
|
||||
// Logged in on a guest route → home or enrollment (pre-only).
|
||||
if (hasToken && guestRoutes.contains(location)) {
|
||||
if (!kIsWeb && !session.isMobileAppSessionUnlocked) {
|
||||
return null;
|
||||
}
|
||||
return '/home';
|
||||
return await TokenService.defaultRouteForStoredTokens();
|
||||
}
|
||||
|
||||
if (!hasToken && guestRoutes.contains(location)) {
|
||||
@ -157,6 +169,8 @@ Future<String?> _mobileReauthRedirectIfNeeded(String location) async {
|
||||
'/sso-login',
|
||||
'/pinSettingPage',
|
||||
'/changePin',
|
||||
'/privacypolicy',
|
||||
'/termsofuse',
|
||||
};
|
||||
final session = SessionManager();
|
||||
if (session.isMobileAppSessionUnlocked) return null;
|
||||
@ -371,6 +385,8 @@ Future<void> startApp() async {
|
||||
await tokenRedirectLogic(context, state),
|
||||
);
|
||||
|
||||
registerAppGoRouter(router);
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
child: MyApp(router: router),
|
||||
|
||||
@ -1364,14 +1364,10 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
|
||||
// ? _size.height * 0.1
|
||||
// : _size.height * 0.2,
|
||||
// ),
|
||||
|
||||
SizedBox(
|
||||
height: Responsive.isDesktop(context)
|
||||
? _size.height * 0.2
|
||||
: _size.height * 0.3,
|
||||
height: 15,
|
||||
),
|
||||
// SizedBox(
|
||||
// height: _size.height * 0.1,
|
||||
// ),
|
||||
Container(
|
||||
alignment: Responsive.isDesktop(context) ? Alignment.center : Alignment.bottomCenter,
|
||||
padding:
|
||||
@ -1411,6 +1407,11 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Responsive.isDesktop(context)
|
||||
? _size.height * 0.2
|
||||
: _size.height * 0.3,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../config/environment.dart';
|
||||
@ -526,10 +525,7 @@ class ApiService {
|
||||
}
|
||||
|
||||
Future<void> _clearLocalStorageAndRedirect() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.clear();
|
||||
// Assuming you have access to the context
|
||||
ToastHelper.showErrorToast(context, 'Session Out');
|
||||
Navigator.pushNamed(context, 'phone');
|
||||
if (!context.mounted) return;
|
||||
await TokenService().logout(context);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@ -14,6 +15,7 @@ import 'dart:io';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../config/environment.dart';
|
||||
import '../customAppBar/nhance_loading_screen.dart';
|
||||
import '../customAppBar/responsive.dart';
|
||||
import '../customAppBar/toastHelper.dart';
|
||||
import '../models/platform_helper_mobile.dart'
|
||||
@ -56,12 +58,14 @@ class _loginState extends State<login> {
|
||||
late String _verificationId;
|
||||
int? _resendToken;
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingSAML = false;
|
||||
bool isEmailFieldVisible = false;
|
||||
bool _obscurePassword = true;
|
||||
bool _resetObscurePassword = true;
|
||||
bool _obscureConfirmPassword = true;
|
||||
late SessionManager session;
|
||||
Map<String, dynamic>? _ssoReturnPayloadCaptured;
|
||||
bool _isProcessingSsoReturn = false;
|
||||
bool clickedForgotPassword = false;
|
||||
bool resetPasswordEnable = false;
|
||||
bool otpFieldShow = false;
|
||||
@ -87,12 +91,30 @@ class _loginState extends State<login> {
|
||||
bool get isPasswordValid =>
|
||||
hasMinLength && hasUpperLower && hasNumber && hasSpecialChar;
|
||||
|
||||
final TapGestureRecognizer _privacyPolicyTap = TapGestureRecognizer();
|
||||
final TapGestureRecognizer _termsOfUseTap = TapGestureRecognizer();
|
||||
|
||||
static final TextStyle _legalLinkStyle = GoogleFonts.poppins(
|
||||
color: const Color(0xFF00989E),
|
||||
fontSize: 9,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
countryController.text = "+91";
|
||||
super.initState();
|
||||
countryController.text = "+91";
|
||||
_privacyPolicyTap.onTap = () {
|
||||
if (mounted) context.go('/privacypolicy');
|
||||
};
|
||||
_termsOfUseTap.onTap = () {
|
||||
if (mounted) context.go('/termsofuse');
|
||||
};
|
||||
if (kIsWeb) {
|
||||
_captureSsoPayloadFromBrowserUrl();
|
||||
if (_ssoReturnPayloadCaptured != null) {
|
||||
_isProcessingSsoReturn = true;
|
||||
}
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => _tryConsumeSsoPayloadFromReturnUrl());
|
||||
}
|
||||
@ -138,15 +160,19 @@ class _loginState extends State<login> {
|
||||
|
||||
if (decoded == null || !mounted) return;
|
||||
|
||||
if (mounted) setState(() => _isProcessingSsoReturn = true);
|
||||
try {
|
||||
await completeLoginFromSsoPayloadMap(context, decoded);
|
||||
} finally {
|
||||
if (kIsWeb) stripPayloadParamFromBrowserUrl();
|
||||
if (mounted) setState(() => _isProcessingSsoReturn = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_privacyPolicyTap.dispose();
|
||||
_termsOfUseTap.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -184,7 +210,7 @@ class _loginState extends State<login> {
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _isLoading = true);
|
||||
// setState(() => _isLoadingSAML = true);
|
||||
|
||||
final base = Uri.parse(Environment.SsoLogin);
|
||||
final qp = <String, String>{'email': emailInput};
|
||||
@ -196,6 +222,13 @@ class _loginState extends State<login> {
|
||||
}
|
||||
final samlEntry = base.replace(queryParameters: qp);
|
||||
|
||||
final samlError = await fetchSamlEntryFailureMessage(samlEntry);
|
||||
if (!mounted) return;
|
||||
if (samlError != null) {
|
||||
ToastHelper.showErrorToast(context, samlError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
skipLoadingReset = true;
|
||||
ssoRedirectSameTab(samlEntry.toString());
|
||||
@ -211,8 +244,9 @@ class _loginState extends State<login> {
|
||||
|
||||
if (!mounted) return;
|
||||
if (result != null && result.isNotEmpty) {
|
||||
if (mounted) setState(() => _isProcessingSsoReturn = true);
|
||||
await completeLoginFromSsoPayloadMap(context, result);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ToastHelper.showErrorToast(
|
||||
@ -221,7 +255,7 @@ class _loginState extends State<login> {
|
||||
}
|
||||
} finally {
|
||||
if (mounted && !skipLoadingReset) {
|
||||
setState(() => _isLoading = false);
|
||||
// setState(() => _isLoadingSAML = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1375,6 +1409,10 @@ class _loginState extends State<login> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isProcessingSsoReturn) {
|
||||
return const NhanceLoadingScreen();
|
||||
}
|
||||
|
||||
Size _size = MediaQuery.of(context).size;
|
||||
EdgeInsets marginInsets = EdgeInsets.zero;
|
||||
|
||||
@ -1779,7 +1817,7 @@ class _loginState extends State<login> {
|
||||
width: double.infinity,
|
||||
height: 45,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleSsoLoginButtonTap,
|
||||
onPressed: _handleSsoLoginButtonTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
@ -2490,17 +2528,10 @@ class _loginState extends State<login> {
|
||||
),
|
||||
]
|
||||
],
|
||||
|
||||
SizedBox(
|
||||
height: _size.width <= 1100 ? 0 : 0,
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(
|
||||
height: Responsive.isDesktop(context)
|
||||
? _size.height * 0.1
|
||||
: _size.height * 0.2,
|
||||
),
|
||||
// SizedBox(
|
||||
// height: _size.height * 0.1,
|
||||
// ),
|
||||
Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
padding:
|
||||
@ -2515,32 +2546,13 @@ class _loginState extends State<login> {
|
||||
fontSize: 9,
|
||||
),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors
|
||||
.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
context.go(
|
||||
'/privacypolicy');
|
||||
// Navigator.pushNamed(
|
||||
// context,
|
||||
// 'privacypolicy');
|
||||
},
|
||||
child: Text(
|
||||
'privacy policy ',
|
||||
style:
|
||||
GoogleFonts.poppins(
|
||||
color:
|
||||
Color(0xFF00989E),
|
||||
fontSize: 9,
|
||||
decoration:
|
||||
TextDecoration
|
||||
.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'privacy policy ',
|
||||
style: _legalLinkStyle,
|
||||
recognizer:
|
||||
_privacyPolicyTap,
|
||||
mouseCursor:
|
||||
SystemMouseCursors.click,
|
||||
),
|
||||
TextSpan(
|
||||
text: 'and ',
|
||||
@ -2549,37 +2561,25 @@ class _loginState extends State<login> {
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors
|
||||
.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.go('/termsofuse');
|
||||
// Navigator.pushNamed(
|
||||
// context,
|
||||
// 'termsofuse');
|
||||
},
|
||||
child: Text(
|
||||
'terms of use',
|
||||
style:
|
||||
GoogleFonts.poppins(
|
||||
color:
|
||||
Color(0xFF00989E),
|
||||
fontSize: 9,
|
||||
decoration:
|
||||
TextDecoration
|
||||
.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'terms of use',
|
||||
style: _legalLinkStyle,
|
||||
recognizer: _termsOfUseTap,
|
||||
mouseCursor:
|
||||
SystemMouseCursors.click,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: _size.width <= 1100 ? 0 : 0,
|
||||
),
|
||||
SizedBox(
|
||||
height: Responsive.isDesktop(context)
|
||||
? _size.height * 0.1
|
||||
: _size.height * 0.2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'helpers/browser_href.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:jwt_decode/jwt_decode.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -15,24 +16,38 @@ 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.
|
||||
/// Uses **`/sso-login`** so the app shows a loading screen (not the login form)
|
||||
/// and then navigates straight to home or enrollment after tokens are saved.
|
||||
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';
|
||||
return '$origin/#/sso-login';
|
||||
}
|
||||
var path = Uri.base.path;
|
||||
if (path.endsWith('/')) {
|
||||
path = path.substring(0, path.length - 1);
|
||||
}
|
||||
if (path.endsWith('/sso-login')) {
|
||||
path = path.substring(0, path.length - '/sso-login'.length);
|
||||
}
|
||||
final href = Environment.baseHref;
|
||||
if (href.isEmpty || href == '/') {
|
||||
return '$origin/login';
|
||||
final basePath = (href.isEmpty || href == '/')
|
||||
? (path.isEmpty ? '' : path)
|
||||
: (href.endsWith('/') ? href.substring(0, href.length - 1) : href);
|
||||
if (basePath.isEmpty) {
|
||||
return '$origin/sso-login';
|
||||
}
|
||||
final trimmed =
|
||||
href.endsWith('/') ? href.substring(0, href.length - 1) : href;
|
||||
return '$origin$trimmed/login';
|
||||
return '$origin$basePath/sso-login';
|
||||
}
|
||||
|
||||
/// When SAML returns to `/login?payload=`, send users to [SsoLoginReturnPage].
|
||||
String? redirectLoginWithSsoPayload(GoRouterState state) {
|
||||
if (state.matchedLocation != '/login') return null;
|
||||
final payload = readPayloadFromRouterUri(state.uri);
|
||||
if (payload == null || payload.isEmpty) return null;
|
||||
return '/sso-login?payload=${Uri.encodeQueryComponent(payload)}';
|
||||
}
|
||||
|
||||
/// Decodes the `payload` query value (base64 / base64url) into a JSON map.
|
||||
@ -153,6 +168,52 @@ SsoPayloadJwtDecode decodeJwtClaimsFromSsoPayloadMap(
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses SAML entry API JSON such as
|
||||
/// `{"status":"failed","code":404,"message":"SAML not configured for this domain"}`.
|
||||
String? parseSamlApiFailureMessage(String body) {
|
||||
if (body.trim().isEmpty) return null;
|
||||
try {
|
||||
final decoded = json.decode(body);
|
||||
if (decoded is Map && decoded['status']?.toString() == 'failed') {
|
||||
final msg = decoded['message']?.toString();
|
||||
if (msg != null && msg.isNotEmpty) return msg;
|
||||
return 'SSO sign-in was not successful';
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// GETs the SAML entry URL without following redirects. Returns an error
|
||||
/// message when the server responds with a failed JSON body (e.g. unknown domain).
|
||||
Future<String?> fetchSamlEntryFailureMessage(Uri samlEntry) async {
|
||||
final client = http.Client();
|
||||
try {
|
||||
final request = http.Request('GET', samlEntry)..followRedirects = false;
|
||||
final streamed = await client
|
||||
.send(request)
|
||||
.timeout(const Duration(seconds: 30));
|
||||
final response = await http.Response.fromStream(streamed);
|
||||
|
||||
final jsonError = parseSamlApiFailureMessage(response.body);
|
||||
if (jsonError != null) return jsonError;
|
||||
|
||||
if (response.statusCode >= 300 && response.statusCode < 400) {
|
||||
return null;
|
||||
}
|
||||
if (response.statusCode == 200) return null;
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
return 'Unable to start SSO sign-in';
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
logDebug('SAML entry preflight failed: $e');
|
||||
return null;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
String prettySsoJsonForConsole(Object? value) {
|
||||
if (value == null) return 'null';
|
||||
try {
|
||||
@ -257,14 +318,14 @@ Future<void> completeLoginFromSsoPayloadMap(
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (postToken != null && postToken.isNotEmpty) {
|
||||
if (kIsWeb) stripPayloadParamFromBrowserUrl();
|
||||
|
||||
final destination = await TokenService.defaultRouteForStoredTokens();
|
||||
SessionManager().unlockMobileAppSession();
|
||||
if (destination == '/home') {
|
||||
ToastHelper.showSuccessToast(context, 'Successfully Logged In');
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/home');
|
||||
} else {
|
||||
SessionManager().unlockMobileAppSession();
|
||||
context.go('/empDetails');
|
||||
}
|
||||
if (context.mounted) context.replace(destination);
|
||||
}
|
||||
|
||||
Future<void> completeLoginFromSsoPayloadString(
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../customAppBar/customAppBar.dart';
|
||||
import '../customAppBar/toastHelper.dart';
|
||||
import 'login_saml_auth.dart';
|
||||
|
||||
/// Mobile: in-app WebView for SAML entry; when URL contains `payload=`, decodes and pops.
|
||||
@ -27,6 +29,21 @@ class _LoginSamlWebViewScreenState extends State<LoginSamlWebViewScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _tryFinishFromPageBody() async {
|
||||
if (_finished || !mounted) return;
|
||||
try {
|
||||
final raw = await _controller.runJavaScriptReturningResult(
|
||||
'document.body ? document.body.innerText : ""',
|
||||
);
|
||||
final text = raw.toString().trim();
|
||||
final msg = parseSamlApiFailureMessage(text);
|
||||
if (msg == null || !mounted) return;
|
||||
_finished = true;
|
||||
ToastHelper.showErrorToast(context, msg);
|
||||
Navigator.of(context).pop();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _tryFinishFromHttpError(HttpResponseError error) {
|
||||
if (_finished || !mounted) return;
|
||||
final status = error.response?.statusCode;
|
||||
@ -57,7 +74,10 @@ class _LoginSamlWebViewScreenState extends State<LoginSamlWebViewScreen> {
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
onPageStarted: (String url) => _tryFinishWithUrl(url),
|
||||
onPageFinished: (String url) => _tryFinishWithUrl(url),
|
||||
onPageFinished: (String url) async {
|
||||
_tryFinishWithUrl(url);
|
||||
if (!_finished) await _tryFinishFromPageBody();
|
||||
},
|
||||
onUrlChange: (UrlChange change) {
|
||||
final url = change.url;
|
||||
if (url != null && url.isNotEmpty) {
|
||||
@ -79,7 +99,8 @@ class _LoginSamlWebViewScreenState extends State<LoginSamlWebViewScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Sign in with SSO')),
|
||||
backgroundColor: Colors.white,
|
||||
appBar: const CustomAppBar(logoOnly: true),
|
||||
body: WebViewWidget(controller: _controller),
|
||||
);
|
||||
}
|
||||
|
||||
@ -270,10 +270,10 @@ class _HomeState extends State<Home> {
|
||||
final response = await apiService.getActiveAndInactivePolicyDetails(
|
||||
client_id, empCodeString, status, client_branch_id, mobileNo,emailId);
|
||||
logDebug('check 1');
|
||||
setState(() {
|
||||
isLoadingGif = false;
|
||||
});
|
||||
if (response['status'] == 'success') {
|
||||
setState(() {
|
||||
isLoadingGif = false;
|
||||
});
|
||||
emp_name = response['emp_name'];
|
||||
policyList = List.from(response['data'] ?? []);
|
||||
retailPolicyDetails = List.from(response['retail_policy_data'] ?? []);
|
||||
@ -307,7 +307,6 @@ class _HomeState extends State<Home> {
|
||||
} else {
|
||||
setState(() {
|
||||
policyDataIsEmpty = 0;
|
||||
isLoadingGif = false;
|
||||
});
|
||||
logDebug('API request failed with status: ${response['status']}');
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ class _policiesState extends State<policies> {
|
||||
String convertDateFormat(String date) {
|
||||
// Define input and output date formats
|
||||
DateFormat inputFormat = DateFormat('dd-MMM-yyyy');
|
||||
DateFormat outputFormat = DateFormat('d MMM yyyy');
|
||||
DateFormat outputFormat = DateFormat('d MMM yyyy');
|
||||
|
||||
// Parse the input date string
|
||||
DateTime parsedDate = inputFormat.parse(date);
|
||||
|
||||
@ -10,7 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../config/environment.dart';
|
||||
import '../../customAppBar/toastHelper.dart';
|
||||
import '../../logger.dart';
|
||||
import '../../models/platform_helper_mobile.dart';
|
||||
import '../../router/app_router.dart';
|
||||
import 'SessionManager.dart';
|
||||
import 'web_session_storage.dart';
|
||||
|
||||
@ -70,6 +70,15 @@ class TokenService {
|
||||
(pre != null && pre.isNotEmpty);
|
||||
}
|
||||
|
||||
/// Post token → home; pre-only → enrollment ([empDetails]).
|
||||
static Future<String> defaultRouteForStoredTokens() async {
|
||||
final post = await getPostToken();
|
||||
if (post != null && post.isNotEmpty) return '/home';
|
||||
final pre = await getPreToken();
|
||||
if (pre != null && pre.isNotEmpty) return '/empDetails';
|
||||
return '/login';
|
||||
}
|
||||
|
||||
/// Clears auth tokens that older web builds stored in SharedPreferences (localStorage).
|
||||
/// Call once at startup so they cannot be mistaken for a valid session.
|
||||
static Future<void> clearLegacyWebAuthFromSharedPreferences() async {
|
||||
@ -131,17 +140,35 @@ class TokenService {
|
||||
return null;
|
||||
}
|
||||
|
||||
void _goAfterLogout(BuildContext context, String location) {
|
||||
if (context.mounted && GoRouter.maybeOf(context) != null) {
|
||||
context.go(location);
|
||||
} else {
|
||||
navigateAppTo(location);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout(BuildContext context) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
String? mobileNumber = prefs.getString('empMobileNo');
|
||||
String? mailID = prefs.getString('empEmailid');
|
||||
final isMpinSkipped = await checkLoginPin();
|
||||
logDebug('isMpinSkipped $isMpinSkipped');
|
||||
|
||||
final postToken = await getPostToken();
|
||||
final hasPostToken = postToken != null && postToken.isNotEmpty;
|
||||
|
||||
String? isMpinSkipped;
|
||||
if (!kIsWeb && hasPostToken) {
|
||||
isMpinSkipped = await checkLoginPin();
|
||||
}
|
||||
logDebug('isMpinSkipped $isMpinSkipped hasPostToken $hasPostToken');
|
||||
|
||||
await SessionManager().clear();
|
||||
|
||||
if (isMobilePlatform()) {
|
||||
if (kIsWeb) {
|
||||
await prefs.clear();
|
||||
_goAfterLogout(context, '/login');
|
||||
} else {
|
||||
if (mobileNumber != null) {
|
||||
await prefs.setString('empMobileNo', mobileNumber);
|
||||
}
|
||||
@ -151,15 +178,11 @@ class TokenService {
|
||||
if (isMpinSkipped != null) {
|
||||
await prefs.setString('is_mpin_skipped', isMpinSkipped);
|
||||
}
|
||||
if (isMpinSkipped != null && isMpinSkipped == '0') {
|
||||
context.go('/pinPage');
|
||||
if (hasPostToken && isMpinSkipped != null && isMpinSkipped == '0') {
|
||||
_goAfterLogout(context, '/pinPage');
|
||||
} else {
|
||||
context.go('/login');
|
||||
_goAfterLogout(context, '/login');
|
||||
}
|
||||
} else {
|
||||
await prefs.clear();
|
||||
ToastHelper.showSuccessToast(context, 'logout');
|
||||
context.go('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../customAppBar/nhance_loading_screen.dart';
|
||||
import '../customAppBar/toastHelper.dart';
|
||||
import '../logger.dart';
|
||||
import 'helpers/browser_href.dart';
|
||||
@ -50,13 +51,15 @@ class _SsoLoginReturnPageState extends State<SsoLoginReturnPage> {
|
||||
}
|
||||
|
||||
logDebug('SSO return: decoded payload (keys)=${decoded.keys.toList()}');
|
||||
await completeLoginFromSsoPayloadMap(context, decoded);
|
||||
try {
|
||||
await completeLoginFromSsoPayloadMap(context, decoded);
|
||||
} finally {
|
||||
if (kIsWeb) stripPayloadParamFromBrowserUrl();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
return const NhanceLoadingScreen();
|
||||
}
|
||||
}
|
||||
|
||||
17
lib/router/app_router.dart
Normal file
17
lib/router/app_router.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Set once in [startApp] so services (e.g. [TokenService.logout]) can navigate
|
||||
/// when [BuildContext] has no [GoRouter] ancestor (e.g. ApiService from MyApp).
|
||||
GoRouter? appGoRouter;
|
||||
|
||||
void registerAppGoRouter(GoRouter router) {
|
||||
appGoRouter = router;
|
||||
}
|
||||
|
||||
void navigateAppTo(String location) {
|
||||
final router = appGoRouter;
|
||||
if (router == null) {
|
||||
throw StateError('GoRouter not registered. Call registerAppGoRouter first.');
|
||||
}
|
||||
router.go(location);
|
||||
}
|
||||
@ -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
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
#version: 1.2.41+98
|
||||
version: 1.0.35+41
|
||||
#version: 2.0.23+61
|
||||
version: 1.0.37+43
|
||||
#version: 2.0.25+63
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.3 <4.0.0'
|
||||
|
||||
@ -4,22 +4,6 @@
|
||||
|
||||
<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 content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="Nhance Benefits">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user