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