SSO and chatbot

This commit is contained in:
Surendiran 2026-06-11 09:25:55 +05:30
parent a157694897
commit ab44ad5815
7 changed files with 126 additions and 73 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 = '63' flutterVersionCode = '65'
} }
def flutterVersionName = localProperties.getProperty('flutter.versionName') def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) { if (flutterVersionName == null) {
flutterVersionName = '2.0.25' flutterVersionName = '2.0.27'
} }
def keystoreProperties = new Properties() def keystoreProperties = new Properties()

View File

@ -5,7 +5,7 @@ class Environment {
static Flavor flavor = Flavor.dev; // overwritten by each main_*.dart static Flavor flavor = Flavor.dev; // overwritten by each main_*.dart
/// Chatbot FAB + window. Set to `true` when ready to enable. /// Chatbot FAB + window. Set to `true` when ready to enable.
static const bool chatbotEnabled = true; static const bool chatbotEnabled = false;
static bool get isProd => flavor == Flavor.prod || flavor == Flavor.prod1; static bool get isProd => flavor == Flavor.prod || flavor == Flavor.prod1;

View File

@ -1,9 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:nhance_app_pwa/features/chatbot/data/chatbot_api_service.dart'; import 'package:nhance_app_pwa/features/chatbot/data/chatbot_api_service.dart';
import 'package:nhance_app_pwa/features/chatbot/data/chatbot_repository.dart'; import 'package:nhance_app_pwa/features/chatbot/data/chatbot_repository.dart';
import 'package:nhance_app_pwa/features/chatbot/domain/models/chatbot_models.dart'; import 'package:nhance_app_pwa/features/chatbot/domain/models/chatbot_models.dart';
import 'package:nhance_app_pwa/pages/helpers/ecard_download_notification_service.dart';
import 'package:nhance_app_pwa/pages/helpers/ecard_download_service.dart';
import 'package:nhance_app_pwa/pages/service/TokenService.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
final chatbotRepositoryProvider = Provider<ChatbotRepository>((ref) { final chatbotRepositoryProvider = Provider<ChatbotRepository>((ref) {
@ -98,6 +102,7 @@ class ChatbotUiState {
class ChatbotController extends StateNotifier<ChatbotUiState> { class ChatbotController extends StateNotifier<ChatbotUiState> {
final ChatbotRepository _repository; final ChatbotRepository _repository;
final EcardDownloadService _ecardDownloadService = const EcardDownloadService();
List<ChatbotPolicy>? _policiesCache; List<ChatbotPolicy>? _policiesCache;
DateTime? _policiesCacheAt; DateTime? _policiesCacheAt;
static const Duration _cacheTtl = Duration(minutes: 10); static const Duration _cacheTtl = Duration(minutes: 10);
@ -201,7 +206,7 @@ class ChatbotController extends StateNotifier<ChatbotUiState> {
showCoveredMembers(); showCoveredMembers();
return; return;
case 'ecard_download': case 'ecard_download':
await openEcard(); await openEcard(context);
return; return;
case 'covered_hospital_list': case 'covered_hospital_list':
await openHospitalList(); await openHospitalList();
@ -298,19 +303,31 @@ class ChatbotController extends StateNotifier<ChatbotUiState> {
void selectPolicy(ChatbotPolicy policy) { void selectPolicy(ChatbotPolicy policy) {
_appendBotMessage('Selected ${policy.policyName}.'); _appendBotMessage('Selected ${policy.policyName}.');
final isGmcPolicy = _isGmcPolicy(policy);
final actionOptions = <ChatbotOption>[
const ChatbotOption(id: 'covered_member_list', label: 'Covered Member List'),
if (isGmcPolicy)
const ChatbotOption(id: 'ecard_download', label: 'E-Card Download'),
if (isGmcPolicy)
const ChatbotOption(
id: 'covered_hospital_list',
label: 'Covered Hospital List',
),
const ChatbotOption(id: 'back_to_policies', label: 'Back to Policies'),
];
state = state.copyWith( state = state.copyWith(
selectedPolicy: policy, selectedPolicy: policy,
step: ChatbotFlowStep.policyActions, step: ChatbotFlowStep.policyActions,
options: const [ options: actionOptions,
ChatbotOption(id: 'covered_member_list', label: 'Covered Member List'),
ChatbotOption(id: 'ecard_download', label: 'E-Card Download'),
ChatbotOption(id: 'covered_hospital_list', label: 'Covered Hospital List'),
ChatbotOption(id: 'back_to_policies', label: 'Back to Policies'),
],
clearError: true, clearError: true,
); );
} }
bool _isGmcPolicy(ChatbotPolicy policy) {
final normalizedType = policy.policyType.trim().toUpperCase();
return normalizedType.startsWith('GMC');
}
void showCoveredMembers() { void showCoveredMembers() {
final policy = state.selectedPolicy; final policy = state.selectedPolicy;
if (policy == null) { if (policy == null) {
@ -328,7 +345,7 @@ class ChatbotController extends StateNotifier<ChatbotUiState> {
} }
} }
Future<void> openEcard() async { Future<void> openEcard(BuildContext context) async {
final policy = state.selectedPolicy; final policy = state.selectedPolicy;
if (policy == null) { if (policy == null) {
_appendBotMessage('Please choose a policy first.'); _appendBotMessage('Please choose a policy first.');
@ -347,12 +364,43 @@ class ChatbotController extends StateNotifier<ChatbotUiState> {
return; return;
} }
final launched = await launchUrl(uri, mode: LaunchMode.externalApplication); final fileName = _chatbotEcardFileName(policy);
if (!launched) { final postToken = await TokenService.getPostToken();
_appendBotMessage('Unable to open E-Card link.'); final headers = <String, String>{
'APP-SIGNATURE': 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
if (postToken != null && postToken.isNotEmpty)
'Authorization': 'Bearer $postToken',
};
if (!kIsWeb) {
_appendBotMessage('Downloading eCard...');
}
final result = await _ecardDownloadService.downloadEcard(
url: uri.toString(),
fileName: fileName,
headers: headers,
);
if (!result.success) {
_appendBotMessage(result.message ?? 'Unable to download E-Card.');
return; return;
} }
_appendBotMessage('Opened E-Card in browser.');
if (!kIsWeb && result.savedPath != null && result.savedPath!.isNotEmpty) {
await EcardDownloadNotificationService.showDownloadCompleted(
filePath: result.savedPath!,
fileName: fileName,
);
}
_appendBotMessage(result.message ?? 'E-Card downloaded successfully.');
}
String _chatbotEcardFileName(ChatbotPolicy policy) {
final sanitized = policy.clientPolicyId.replaceAll(RegExp(r'[^\w\-.]'), '_');
if (sanitized.isEmpty) return 'ecard.pdf';
return 'ecard_$sanitized.pdf';
} }
Future<void> openHospitalList() async { Future<void> openHospitalList() async {

View File

@ -21,6 +21,7 @@ class PolicyMember {
class ChatbotPolicy { class ChatbotPolicy {
final String clientPolicyId; final String clientPolicyId;
final String policyName; final String policyName;
final String policyType;
final String eCardDownloadUrl; final String eCardDownloadUrl;
final String networkHospitalsUrl; final String networkHospitalsUrl;
final List<PolicyMember> members; final List<PolicyMember> members;
@ -28,6 +29,7 @@ class ChatbotPolicy {
const ChatbotPolicy({ const ChatbotPolicy({
required this.clientPolicyId, required this.clientPolicyId,
required this.policyName, required this.policyName,
required this.policyType,
required this.eCardDownloadUrl, required this.eCardDownloadUrl,
required this.networkHospitalsUrl, required this.networkHospitalsUrl,
required this.members, required this.members,
@ -38,6 +40,8 @@ class ChatbotPolicy {
return ChatbotPolicy( return ChatbotPolicy(
clientPolicyId: (json['client_policy_id'] ?? '').toString(), clientPolicyId: (json['client_policy_id'] ?? '').toString(),
policyName: (json['heading'] ?? 'Unnamed Policy').toString(), policyName: (json['heading'] ?? 'Unnamed Policy').toString(),
policyType:
(json['policy_type'] ?? json['policy_type_long_name'] ?? '').toString(),
eCardDownloadUrl: (json['eCardDownload'] ?? '').toString(), eCardDownloadUrl: (json['eCardDownload'] ?? '').toString(),
networkHospitalsUrl: (json['network_hospitals_url'] ?? '').toString(), networkHospitalsUrl: (json['network_hospitals_url'] ?? '').toString(),
members: membersRaw members: membersRaw

View File

@ -126,10 +126,12 @@ Future<DownloadResult> _downloadAndShareOnIos({
return DownloadResult.failure('Download failed'); return DownloadResult.failure('Download failed');
} }
await Share.shareXFiles( await SharePlus.instance.share(
[XFile(filePath)], ShareParams(
text: 'Save eCard to Files', files: [XFile(filePath)],
subject: safeFileName, text: 'Save eCard to Files',
subject: safeFileName,
),
); );
return DownloadResult.success( return DownloadResult.success(

View File

@ -1877,64 +1877,63 @@ class _loginState extends State<login> {
), ),
), ),
), ),
SizedBox(height: 15), if (_isSsoButtonEnabled) ...[
Container( SizedBox(height: 15),
margin: Responsive.isDesktop(context) Container(
? const EdgeInsets.symmetric(horizontal: 150) margin: Responsive.isDesktop(context)
: const EdgeInsets.symmetric(horizontal: 0), ? const EdgeInsets.symmetric(horizontal: 150)
child: SizedBox( : const EdgeInsets.symmetric(horizontal: 0),
width: double.infinity, child: SizedBox(
height: 45, width: double.infinity,
child: ElevatedButton( height: 45,
onPressed: _isSsoButtonEnabled && child: ElevatedButton(
!_isLoadingSAML onPressed: !_isLoadingSAML
? _handleSsoLoginButtonTap ? _handleSsoLoginButtonTap
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.white, backgroundColor: Colors.white,
foregroundColor: Colors.black, foregroundColor: Colors.black,
disabledBackgroundColor: disabledBackgroundColor:
const Color(0xFFF5F5F5), const Color(0xFFF5F5F5),
disabledForegroundColor: disabledForegroundColor:
Colors.black38, Colors.black38,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 24, horizontal: 24,
vertical: 5, vertical: 5,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(
color: _isSsoButtonEnabled
? Colors.black
: Colors.black26,
width: 0.5,
), ),
), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(18),
child: Row( side: const BorderSide(
mainAxisAlignment: MainAxisAlignment.center, color: Colors.black,
mainAxisSize: MainAxisSize.min, width: 0.5,
children: [
Text(
"Sign In With Microsoft",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 13,
), ),
), ),
const SizedBox(width: 8), ),
Image.asset( child: Row(
'assets/images/login/microsoft.png', mainAxisAlignment: MainAxisAlignment.center,
width: 22, mainAxisSize: MainAxisSize.min,
height: 22, children: [
fit: BoxFit.contain, Text(
), "Sign In With Microsoft",
], style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
const SizedBox(width: 8),
Image.asset(
'assets/images/login/microsoft.png',
width: 22,
height: 22,
fit: BoxFit.contain,
),
],
),
), ),
), ),
), ),
), SizedBox(height: 15),
SizedBox(height: 15), ],
// MouseRegion( // MouseRegion(
// cursor: // cursor:
// SystemMouseCursors.click, // SystemMouseCursors.click,

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.37+43 version: 1.0.38+44
#version: 2.0.25+63 #version: 2.0.27+65
environment: environment:
sdk: '>=3.3.3 <4.0.0' sdk: '>=3.3.3 <4.0.0'