site , signup and demo completed
parent
527ba1eb19
commit
f116c96578
@ -0,0 +1,18 @@
|
||||
/// Enum for OTP delivery methods
|
||||
enum OtpMethod {
|
||||
sms('SMS'),
|
||||
email('Email');
|
||||
|
||||
final String value;
|
||||
|
||||
const OtpMethod(this.value);
|
||||
|
||||
/// Get OTP method from string value
|
||||
static OtpMethod fromString(String value) {
|
||||
return OtpMethod.values.firstWhere(
|
||||
(method) => method.value.toLowerCase() == value.toLowerCase(),
|
||||
orElse: () => OtpMethod.sms,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,117 @@
|
||||
/// Model representing an application demo request
|
||||
class ApplicationDemoRequestModel {
|
||||
final String? id;
|
||||
final String fullName;
|
||||
final String emailAddress;
|
||||
final String mobileNumber;
|
||||
final String? companyName;
|
||||
final int? companyId;
|
||||
final List<String> roleIds;
|
||||
final List<String> roleNames;
|
||||
final String status; // Pending, Approved, Rejected
|
||||
final DateTime requestDate;
|
||||
|
||||
ApplicationDemoRequestModel({
|
||||
this.id,
|
||||
required this.fullName,
|
||||
required this.emailAddress,
|
||||
required this.mobileNumber,
|
||||
this.companyName,
|
||||
this.companyId,
|
||||
required this.roleIds,
|
||||
required this.roleNames,
|
||||
required this.status,
|
||||
required this.requestDate,
|
||||
});
|
||||
|
||||
/// Converts model to JSON for API submission
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id ?? 0,
|
||||
'fullName': fullName,
|
||||
'emailAddress': emailAddress,
|
||||
'mobileNumber': mobileNumber,
|
||||
// 'companyName': companyId ?? 88,
|
||||
'companyId': 88,
|
||||
'demoRequestUserRoleIds': roleIds,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates model from API response JSON
|
||||
factory ApplicationDemoRequestModel.fromJson(Map<String, dynamic> json) {
|
||||
return ApplicationDemoRequestModel(
|
||||
id: json['id']?.toString() ?? json['requestNumber']?.toString() ?? '',
|
||||
fullName: json['fullName'] ?? '',
|
||||
emailAddress: json['emailAddress'] ?? json['email'] ?? '',
|
||||
mobileNumber: json['mobileNumber'] ?? json['mobile'] ?? '',
|
||||
companyName: json['companyName'] ?? json['company'] ?? '',
|
||||
companyId: json['companyId'],
|
||||
roleIds: _parseList(json['roleIds'] ?? json['demoRequestUserRoleIds']),
|
||||
roleNames: _parseList(json['roleNames'] ?? json['roles']),
|
||||
status: json['status'] ?? 'Pending',
|
||||
requestDate: _parseDate(json),
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper method to parse list from various formats
|
||||
static List<String> _parseList(dynamic value) {
|
||||
if (value == null) return [];
|
||||
|
||||
if (value is List) {
|
||||
return value.map((e) => e.toString()).toList();
|
||||
}
|
||||
|
||||
if (value is String) {
|
||||
if (value.isEmpty) return [];
|
||||
return value.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Helper method to parse date from various formats
|
||||
static DateTime _parseDate(Map<String, dynamic> json) {
|
||||
final dateValue = json['requestDate'] ?? json['createdDate'] ?? json['date'];
|
||||
|
||||
if (dateValue == null) return DateTime.now();
|
||||
|
||||
if (dateValue is String) {
|
||||
try {
|
||||
return DateTime.parse(dateValue);
|
||||
} catch (e) {
|
||||
return DateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
if (dateValue is DateTime) return dateValue;
|
||||
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
/// Creates a copy with optional field updates
|
||||
ApplicationDemoRequestModel copyWith({
|
||||
String? id,
|
||||
String? fullName,
|
||||
String? emailAddress,
|
||||
String? mobileNumber,
|
||||
String? companyName,
|
||||
int? companyId,
|
||||
List<String>? roleIds,
|
||||
List<String>? roleNames,
|
||||
String? status,
|
||||
DateTime? requestDate,
|
||||
}) {
|
||||
return ApplicationDemoRequestModel(
|
||||
id: id ?? this.id,
|
||||
fullName: fullName ?? this.fullName,
|
||||
emailAddress: emailAddress ?? this.emailAddress,
|
||||
mobileNumber: mobileNumber ?? this.mobileNumber,
|
||||
companyName: companyName ?? this.companyName,
|
||||
companyId: companyId ?? this.companyId,
|
||||
roleIds: roleIds ?? this.roleIds,
|
||||
roleNames: roleNames ?? this.roleNames,
|
||||
status: status ?? this.status,
|
||||
requestDate: requestDate ?? this.requestDate,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import 'package:test_sa/models/base.dart';
|
||||
|
||||
class ApplicationDemoRoleModel extends Base {
|
||||
final int? value;
|
||||
final String identifier;
|
||||
|
||||
ApplicationDemoRoleModel({
|
||||
required String id,
|
||||
required String name,
|
||||
this.value,
|
||||
}) : identifier = id.toString(),
|
||||
super(identifier: id, name: name);
|
||||
|
||||
factory ApplicationDemoRoleModel.fromJson(Map<String, dynamic> json) {
|
||||
return ApplicationDemoRoleModel(
|
||||
id: json['id'] ?? '',
|
||||
name: json['name'] ?? '',
|
||||
value: json['fixedName'] != null ? int.tryParse(json['fixedName'].toString()) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,222 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:test_sa/controllers/api_routes/urls.dart';
|
||||
import 'package:test_sa/modules/applicationDemo/models/application_demo_request_model.dart';
|
||||
import 'package:test_sa/modules/applicationDemo/models/role_model.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// Provider for managing application demo request operations
|
||||
class ApplicationDemoRequestProvider extends ChangeNotifier {
|
||||
bool _loading = false;
|
||||
bool _roleLoading = false;
|
||||
List<ApplicationDemoRoleModel> _roles = [];
|
||||
String? _errorMessage;
|
||||
String? _successMessage;
|
||||
|
||||
// Getters
|
||||
bool get loading => _loading;
|
||||
|
||||
bool get roleLoading => _roleLoading;
|
||||
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
String? get successMessage => _successMessage;
|
||||
|
||||
List<ApplicationDemoRoleModel> get roles => _roles;
|
||||
|
||||
// Setter
|
||||
set loading(bool value) {
|
||||
_loading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
set roleLoading(bool value) {
|
||||
_loading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Get headers without authentication token for public APIs
|
||||
Map<String, String> get _unauthenticatedHeaders => {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Timezone-Offset': DateTime.now().timeZoneOffset.toString().split(".").first,
|
||||
};
|
||||
|
||||
/// Fetches available roles from the server (without authentication)
|
||||
Future<int> fetchRoles() async {
|
||||
if (roleLoading) return -2;
|
||||
|
||||
roleLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse(URLs.getApplicationDemoRoles),
|
||||
headers: _unauthenticatedHeaders,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final List<dynamic> responseData = jsonDecode(response.body);
|
||||
_roles = responseData.map((json) => ApplicationDemoRoleModel.fromJson(json)).toList();
|
||||
|
||||
roleLoading = false;
|
||||
notifyListeners();
|
||||
return response.statusCode;
|
||||
} else {
|
||||
final responseData = jsonDecode(response.body);
|
||||
_errorMessage = responseData['message'] ?? 'Failed to fetch roles';
|
||||
roleLoading = false;
|
||||
notifyListeners();
|
||||
return response.statusCode;
|
||||
}
|
||||
} catch (error) {
|
||||
_errorMessage = 'Failed to fetch roles: $error';
|
||||
roleLoading = false;
|
||||
notifyListeners();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Submits an application demo request to the server (without authentication)
|
||||
Future<bool> submitApplicationDemoRequest(ApplicationDemoRequestModel request) async {
|
||||
loading = true;
|
||||
_errorMessage = null;
|
||||
_successMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final requestBody = request.toJson();
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(URLs.submitApplicationDemoRequest),
|
||||
headers: _unauthenticatedHeaders,
|
||||
body: json.encode(requestBody),
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final responseData = json.decode(response.body);
|
||||
_successMessage = responseData['message'] ?? 'Your demo request has been submitted successfully';
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
final responseData = json.decode(response.body);
|
||||
_errorMessage = responseData['message'] ?? 'Failed to submit request';
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
_errorMessage = 'Failed to submit request: $error';
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends OTP to user via email or SMS
|
||||
Future<bool> sendOtp({
|
||||
required String fullName,
|
||||
required String phoneNumber,
|
||||
required String email,
|
||||
required String typeSend, // "SMS" or "Email"
|
||||
}) async {
|
||||
loading = true;
|
||||
_errorMessage = null;
|
||||
_successMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final requestBody = {
|
||||
'fullName': fullName,
|
||||
'phoneNumber': phoneNumber,
|
||||
'email': email,
|
||||
'typeSend': typeSend,
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(URLs.sendDemoUserOtp),
|
||||
headers: _unauthenticatedHeaders,
|
||||
body: json.encode(requestBody),
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final responseData = json.decode(response.body);
|
||||
_successMessage = responseData['message'];
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
final responseData = json.decode(response.body);
|
||||
_errorMessage = responseData['message'] ?? 'Failed to send OTP';
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
_errorMessage = 'Failed to send OTP: $error';
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies OTP code
|
||||
Future<bool> verifyOtp({
|
||||
String? email,
|
||||
String? phoneNumber,
|
||||
required String otpCode,
|
||||
}) async {
|
||||
loading = true;
|
||||
_errorMessage = null;
|
||||
_successMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final requestBody = <String, dynamic>{
|
||||
'otpCode': otpCode,
|
||||
};
|
||||
|
||||
// Add email or phoneNumber based on what's provided
|
||||
if (email != null && email.isNotEmpty) {
|
||||
requestBody['email'] = email;
|
||||
}
|
||||
if (phoneNumber != null && phoneNumber.isNotEmpty) {
|
||||
requestBody['phoneNumber'] = phoneNumber;
|
||||
}
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(URLs.verifyDemoUserOtp),
|
||||
headers: _unauthenticatedHeaders,
|
||||
body: json.encode(requestBody),
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final responseData = json.decode(response.body);
|
||||
_successMessage = responseData['message'];
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
final responseData = json.decode(response.body);
|
||||
_errorMessage = responseData['message'] ?? 'Invalid OTP code';
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
_errorMessage = 'Failed to verify OTP: $error';
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the provider state
|
||||
void reset() {
|
||||
_loading = false;
|
||||
_roles.clear();
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,309 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:test_sa/controllers/validator/validator.dart';
|
||||
import 'package:test_sa/extensions/context_extension.dart';
|
||||
import 'package:test_sa/extensions/int_extensions.dart';
|
||||
import 'package:test_sa/extensions/widget_extensions.dart';
|
||||
import 'package:test_sa/modules/applicationDemo/enums/otp_method.dart';
|
||||
import 'package:test_sa/modules/applicationDemo/models/application_demo_request_model.dart';
|
||||
import 'package:test_sa/modules/applicationDemo/models/role_model.dart';
|
||||
import 'package:test_sa/modules/applicationDemo/providers/application_demo_request_provider.dart';
|
||||
import 'package:test_sa/modules/applicationDemo/widgets/otp_method_selection_dialog.dart';
|
||||
import 'package:test_sa/modules/applicationDemo/widgets/otp_verification_bottom_sheet.dart';
|
||||
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
|
||||
import 'package:test_sa/new_views/app_style/app_color.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/custom_app_bar.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/multiple_item_drop_down_menu.dart';
|
||||
import 'package:test_sa/providers/loading_list_notifier.dart';
|
||||
|
||||
class DemoRequestFormScreen extends StatefulWidget {
|
||||
static const String routeName = '/demo_request_form';
|
||||
|
||||
const DemoRequestFormScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DemoRequestFormScreen> createState() => _DemoRequestFormScreenState();
|
||||
}
|
||||
|
||||
class _DemoRequestFormScreenState extends State<DemoRequestFormScreen> {
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController _fullNameController = TextEditingController();
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _mobileController = TextEditingController();
|
||||
final TextEditingController _companyNameController = TextEditingController();
|
||||
|
||||
List<ApplicationDemoRoleModel> _selectedRoles = [];
|
||||
bool _isSubmitting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Fetch roles when the screen initializes
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = Provider.of<ApplicationDemoRequestProvider>(context, listen: false);
|
||||
provider.fetchRoles();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fullNameController.dispose();
|
||||
_emailController.dispose();
|
||||
_mobileController.dispose();
|
||||
_companyNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<ApplicationDemoRequestProvider>(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background(context),
|
||||
appBar: const CustomAppBar(
|
||||
title: 'Demo Request',
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AppTextFormField(
|
||||
controller: _fullNameController,
|
||||
labelText: 'Full Name',
|
||||
hintText: 'Enter your full name',
|
||||
textInputType: TextInputType.name,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Full Name is required';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
16.height,
|
||||
AppTextFormField(
|
||||
controller: _emailController,
|
||||
labelText: 'Email Address',
|
||||
hintText: 'Enter your email address',
|
||||
textInputType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Email is required';
|
||||
if (!Validator.isEmail(value)) return 'Invalid email format';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
16.height,
|
||||
AppTextFormField(
|
||||
controller: _mobileController,
|
||||
labelText: 'Mobile Number',
|
||||
hintText: 'Enter your mobile number',
|
||||
textInputType: TextInputType.phone,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Mobile Number is required';
|
||||
if (!Validator.isPhoneNumber(value)) return 'Invalid mobile number';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
16.height,
|
||||
AppTextFormField(
|
||||
controller: _companyNameController,
|
||||
labelText: 'Company Name',
|
||||
hintText: 'Enter your company name',
|
||||
textInputType: TextInputType.text,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Company Name is required';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
16.height,
|
||||
MultipleItemDropDownMenu<ApplicationDemoRoleModel, NullableLoadingProvider>(
|
||||
context: context,
|
||||
title: 'Select Roles',
|
||||
initialValue: _selectedRoles,
|
||||
staticData: provider.roles,
|
||||
loading: provider.roleLoading,
|
||||
showAsFullScreenDialog: true,
|
||||
onSelect: (selectedRoles) {
|
||||
setState(() {
|
||||
_selectedRoles = selectedRoles ?? [];
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingAll(16).expanded,
|
||||
FooterActionButton.footerContainer(
|
||||
context: context,
|
||||
child: AppFilledButton(
|
||||
label: 'Submit',
|
||||
buttonColor: AppColor.primary10,
|
||||
maxWidth: true,
|
||||
loading: _isSubmitting,
|
||||
onPressed: _isSubmitting ? null : _submitRequest,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submitRequest() async {
|
||||
// Validate form fields
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
if (_selectedRoles.isEmpty) {
|
||||
context.showConfirmDialog('Please select at least one role');
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return OtpMethodSelectionDialog(
|
||||
onMethodSelected: (OtpMethod method) {
|
||||
_submitRequestWithOtpMethod(method);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submitRequestWithOtpMethod(OtpMethod otpMethod) async {
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final provider = Provider.of<ApplicationDemoRequestProvider>(context, listen: false);
|
||||
final otpSent = await provider.sendOtp(
|
||||
fullName: _fullNameController.text.trim(),
|
||||
phoneNumber: _mobileController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
typeSend: otpMethod.value,
|
||||
);
|
||||
|
||||
if (otpSent) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
await context.showBottomSheet(
|
||||
OtpVerificationBottomSheet(
|
||||
otpMethod: otpMethod.value,
|
||||
onOtpVerified: (String otp) async {
|
||||
await _verifyOtpAndComplete(otp, otpMethod);
|
||||
},
|
||||
onResend: () async {
|
||||
await provider.sendOtp(
|
||||
fullName: _fullNameController.text.trim(),
|
||||
phoneNumber: _mobileController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
typeSend: otpMethod.value,
|
||||
);
|
||||
},
|
||||
onChangeMethod: () {
|
||||
_submitRequest();
|
||||
},
|
||||
),
|
||||
isDismissible: false,
|
||||
title: 'Verify OTP',
|
||||
showCancelButton: true,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
context.showConfirmDialog(
|
||||
provider.errorMessage ?? 'Failed to send OTP. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
context.showConfirmDialog('An error occurred: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyOtpAndComplete(String otp, OtpMethod otpMethod) async {
|
||||
// Close the bottom sheet
|
||||
Navigator.pop(context);
|
||||
|
||||
// Show loading
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final provider = Provider.of<ApplicationDemoRequestProvider>(context, listen: false);
|
||||
|
||||
// Verify OTP - pass email or phoneNumber based on selected method
|
||||
final otpVerified = await provider.verifyOtp(
|
||||
email: otpMethod == OtpMethod.email ? _emailController.text.trim() : null,
|
||||
phoneNumber: otpMethod == OtpMethod.sms ? _mobileController.text.trim() : null,
|
||||
otpCode: otp,
|
||||
);
|
||||
|
||||
if (otpVerified) {
|
||||
// OTP verified, now submit the demo request
|
||||
final request = ApplicationDemoRequestModel(
|
||||
fullName: _fullNameController.text.trim(),
|
||||
emailAddress: _emailController.text.trim(),
|
||||
mobileNumber: _mobileController.text.trim(),
|
||||
companyId: 0,
|
||||
roleIds: _selectedRoles.map((role) => role.identifier ?? '').toList(),
|
||||
roleNames: _selectedRoles.map((role) => role.name ?? '').toList(),
|
||||
status: 'Pending',
|
||||
requestDate: DateTime.now(),
|
||||
);
|
||||
|
||||
final success = await provider.submitApplicationDemoRequest(request);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
if (success) {
|
||||
context.showConfirmDialog(
|
||||
provider.successMessage ?? 'Your demo request has been verified and submitted successfully. We will contact you soon!',
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(); // Close dialog
|
||||
Navigator.of(context).pop(); // Go back to login
|
||||
},
|
||||
);
|
||||
} else {
|
||||
context.showConfirmDialog(
|
||||
provider.errorMessage ?? 'Failed to submit demo request. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// OTP verification failed
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
context.showConfirmDialog(
|
||||
provider.errorMessage ?? 'Invalid OTP code. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
context.showConfirmDialog('OTP verification failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:test_sa/extensions/int_extensions.dart';
|
||||
import 'package:test_sa/extensions/text_extensions.dart';
|
||||
import 'package:test_sa/extensions/widget_extensions.dart';
|
||||
import 'package:test_sa/modules/applicationDemo/enums/otp_method.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
|
||||
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
|
||||
|
||||
class OtpMethodSelectionDialog extends StatelessWidget {
|
||||
final Function(OtpMethod method) onMethodSelected;
|
||||
|
||||
const OtpMethodSelectionDialog({
|
||||
Key? key,
|
||||
required this.onMethodSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(),
|
||||
insetPadding: const EdgeInsets.only(left: 21, right: 21),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, top: 18, bottom: 28),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InfoHeader16Widget("Information").paddingOnly(top: 16),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close),
|
||||
color: Colors.black87,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
"Choose a method to receive the OTP".bodyText(context),
|
||||
28.height,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppFilledButton(
|
||||
label: OtpMethod.sms.value,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onMethodSelected(OtpMethod.sms);
|
||||
},
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
16.width,
|
||||
Expanded(
|
||||
child: AppFilledButton(
|
||||
label: OtpMethod.email.value,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onMethodSelected(OtpMethod.email);
|
||||
},
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,175 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinput/pinput.dart';
|
||||
import 'package:test_sa/extensions/int_extensions.dart';
|
||||
import 'package:test_sa/extensions/widget_extensions.dart';
|
||||
import 'package:test_sa/new_views/app_style/app_color.dart';
|
||||
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
|
||||
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
|
||||
|
||||
class OtpVerificationBottomSheet extends StatefulWidget {
|
||||
final String otpMethod;
|
||||
final Function(String otp) onOtpVerified;
|
||||
final VoidCallback? onResend;
|
||||
final VoidCallback? onChangeMethod;
|
||||
|
||||
const OtpVerificationBottomSheet({
|
||||
Key? key,
|
||||
required this.otpMethod,
|
||||
required this.onOtpVerified,
|
||||
this.onResend,
|
||||
this.onChangeMethod,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<OtpVerificationBottomSheet> createState() => _OtpVerificationBottomSheetState();
|
||||
}
|
||||
|
||||
class _OtpVerificationBottomSheetState extends State<OtpVerificationBottomSheet> {
|
||||
final ValueNotifier<String> _timerNotifier = ValueNotifier("2:00");
|
||||
bool _canResend = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTimer();
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
int seconds = 120; // 2 minutes
|
||||
_canResend = false;
|
||||
|
||||
Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (!mounted) return false;
|
||||
|
||||
seconds--;
|
||||
if (seconds <= 0) {
|
||||
_timerNotifier.value = "0:00";
|
||||
_canResend = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
final minutes = seconds ~/ 60;
|
||||
final secs = seconds % 60;
|
||||
_timerNotifier.value = "$minutes:${secs.toString().padLeft(2, '0')}";
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timerNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultPinTheme = PinTheme(
|
||||
width: 51.toScreenWidth,
|
||||
height: 63.toScreenHeight,
|
||||
textStyle: TextStyle(fontSize: 24, color: AppColor.headingTextColor(context)),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.background(context),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppColor.border2Color(context)),
|
||||
),
|
||||
);
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false, // Prevent dismiss by back button
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoTextWidget(
|
||||
label: 'OTP has been sent to your ${widget.otpMethod.toLowerCase()}',
|
||||
isMsg: true,
|
||||
showEmptyValue: true,
|
||||
),
|
||||
Center(
|
||||
child: Pinput(
|
||||
length: 4,
|
||||
defaultPinTheme: defaultPinTheme,
|
||||
focusedPinTheme: defaultPinTheme.copyWith(
|
||||
decoration: defaultPinTheme.decoration?.copyWith(
|
||||
border: Border.all(color: AppColor.primary10),
|
||||
),
|
||||
),
|
||||
onCompleted: (pin) async {
|
||||
// Call the verification callback
|
||||
widget.onOtpVerified(pin);
|
||||
},
|
||||
),
|
||||
).paddingOnly(top: 24, bottom: 24),
|
||||
Row(
|
||||
children: [
|
||||
InfoTextWidget(label: 'Resend in', isMsg: true, showEmptyValue: true),
|
||||
6.width,
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: _timerNotifier,
|
||||
builder: (context, value, _) {
|
||||
return InfoTextWidget(label: value, isMsg: true, showEmptyValue: true);
|
||||
},
|
||||
),
|
||||
6.width,
|
||||
InkWell(
|
||||
onTap: _canResend
|
||||
? () {
|
||||
if (widget.onResend != null) {
|
||||
widget.onResend!();
|
||||
_startTimer();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
'Resend',
|
||||
style: TextStyle(
|
||||
color: _canResend ? AppColor.primary10 : AppColor.white50,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12.toScreenWidth,
|
||||
decorationColor: _canResend ? AppColor.primary10 : AppColor.white50,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).toShadowContainer(context),
|
||||
16.height,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
InfoHeader16Widget('Having trouble receiving OTP?'),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (widget.onChangeMethod != null) {
|
||||
widget.onChangeMethod!();
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'Check out other methods',
|
||||
style: TextStyle(
|
||||
color: AppColor.primary10,
|
||||
fontSize: 16.toScreenWidth,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: AppColor.primary10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue