completed velocity integration
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:qpay/http/http.dart';
|
||||
import 'package:qpay/models/api_response.dart';
|
||||
import 'package:qpay/screens/home/home_controller.dart' as hc;
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart' as tc;
|
||||
@@ -69,21 +70,9 @@ class PayController extends ChangeNotifier {
|
||||
model.selectedProduct = transactionController.model.selectedProduct;
|
||||
}
|
||||
|
||||
void _showErrorSnackBar(String? message) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message.toString()),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.deepOrange,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// formData can come from a repeat transaction
|
||||
// otherwise build it from elements of the normal flow
|
||||
Future<void> doTransaction([tc.FormData? formData]) async {
|
||||
Future<ApiResponse<Map<String, dynamic>>> doTransaction([tc.FormData? formData]) async {
|
||||
try {
|
||||
model.isLoading = true;
|
||||
notifyListeners();
|
||||
@@ -120,52 +109,59 @@ class PayController extends ChangeNotifier {
|
||||
|
||||
dynamic response = workflowResponse['body'];
|
||||
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
if (response['status'] == 'SUCCESS') {
|
||||
model.status = response['status'];
|
||||
|
||||
transactionController.updateConfirmationData(response);
|
||||
return ApiResponse.success(Map<String, dynamic>.from(response));
|
||||
} else {
|
||||
model.status = response['status'];
|
||||
model.errorMessage =
|
||||
response['errorMessage'] ??
|
||||
"Problem with the transaction. Please try again or contact support";
|
||||
_showErrorSnackBar(model.errorMessage);
|
||||
return ApiResponse.failure(model.errorMessage!);
|
||||
}
|
||||
} on DioException catch (e, s) {
|
||||
logger.e(s);
|
||||
_showErrorSnackBar("Network error. Please try again or contact support");
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.failure("Network error. Please try again or contact support");
|
||||
} catch (e, s) {
|
||||
logger.e(s);
|
||||
_showErrorSnackBar(
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.failure(
|
||||
"Problem processing that transaction. Please try again or contact support",
|
||||
);
|
||||
}
|
||||
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> getProducts(String providerId) async {
|
||||
// model.products = getFakeProducts();
|
||||
Future<ApiResponse<List<BillProduct>>> getProducts(String providerId) async {
|
||||
try {
|
||||
model.isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
List<dynamic> response = await http.get(
|
||||
'/public/providers/$providerId/products',
|
||||
);
|
||||
model.products = response.map((e) => BillProduct.fromJson(e)).toList();
|
||||
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.success(model.products);
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
_showErrorSnackBar(
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.failure(
|
||||
"Problem fetching products, are you connected to the internet?",
|
||||
);
|
||||
}
|
||||
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> getPaymentProcessors() async {
|
||||
Future<ApiResponse<List<PaymentProcessor>>> getPaymentProcessors() async {
|
||||
try {
|
||||
model.isLoading = true;
|
||||
model.paymentProcessors = getFakePaymentProcessors();
|
||||
@@ -175,10 +171,14 @@ class PayController extends ChangeNotifier {
|
||||
model.paymentProcessors =
|
||||
response.map((e) => PaymentProcessor.fromJson(e)).toList();
|
||||
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.success(model.paymentProcessors);
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
_showErrorSnackBar(
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.failure(
|
||||
"Problem fetching payment processors, are you connected to the internet?",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,14 +71,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
listen: false,
|
||||
);
|
||||
|
||||
_nameController.text =
|
||||
transactionController.model.formData.creditName ?? '';
|
||||
_emailController.text =
|
||||
transactionController.model.formData.creditEmail ?? '';
|
||||
_amountController.text = transactionController.model.formData.amount;
|
||||
|
||||
// Initialize phone number and country code
|
||||
// updatePhoneNumber(transactionController.model.formData.creditPhone ?? '');
|
||||
_setupData();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
@@ -113,7 +106,6 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
_controller.forward();
|
||||
_setupData();
|
||||
}
|
||||
|
||||
_setupData() async {
|
||||
@@ -121,27 +113,67 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
if (prefs.containsKey('phone')) {
|
||||
updatePhoneNumber(prefs.getString('phone')!);
|
||||
}
|
||||
|
||||
_nameController.text =
|
||||
transactionController.model.formData.creditName ?? '';
|
||||
_emailController.text =
|
||||
transactionController.model.formData.creditEmail ?? '';
|
||||
_amountController.text = transactionController.model.formData.amount;
|
||||
_phoneController.text =
|
||||
transactionController.model.formData.debitPhone ?? '';
|
||||
}
|
||||
|
||||
Future<void> updatePhoneNumber(String phoneNumber) async {
|
||||
String existingPhone = phoneNumber;
|
||||
if (existingPhone.isNotEmpty) {
|
||||
// Try to extract country code from existing phone number
|
||||
for (var country in countryCodes) {
|
||||
if (existingPhone.startsWith(country['code']!)) {
|
||||
setState(() {
|
||||
selectedCountryCode = country['code']!;
|
||||
});
|
||||
_phoneController.text = existingPhone.substring(
|
||||
country['code']!.length,
|
||||
);
|
||||
break;
|
||||
}
|
||||
Future<void> updatePhoneNumber(String newNumber) async {
|
||||
for (var country in countryCodes) {
|
||||
if (newNumber.startsWith(country['code']!)) {
|
||||
setState(() {
|
||||
selectedCountryCode = country['code']!;
|
||||
});
|
||||
_phoneController.text = newNumber.substring(country['code']!.length);
|
||||
break;
|
||||
}
|
||||
// If no country code found, use default and set the full number
|
||||
if (_phoneController.text.isEmpty) {
|
||||
_phoneController.text = existingPhone;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submitPayment(PaymentProcessor e, BuildContext context) async {
|
||||
controller.updateAmount(_amountController.text);
|
||||
if (!controller.model.selectedProvider!.requiresAmount) {
|
||||
controller.updateAmount(
|
||||
controller.model.selectedProduct!.defaultAmount.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
String fullPhoneNumber = _phoneController.text;
|
||||
controller.updatePhone(fullPhoneNumber);
|
||||
|
||||
if (showAdditionalRecipientDetails) {
|
||||
controller.updateAdditionalRecipientDetails(
|
||||
_nameController.text,
|
||||
_emailController.text,
|
||||
);
|
||||
}
|
||||
controller.updateSelectedPaymentProcessor(e);
|
||||
transactionController.updateSelectedPaymentProcessor(e);
|
||||
|
||||
final apiResponse = await controller.doTransaction();
|
||||
|
||||
if (!apiResponse.isSuccess) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
controller.model.errorMessage ??
|
||||
apiResponse.error ??
|
||||
"Transaction failed",
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
width: 600,
|
||||
backgroundColor: Colors.deepOrange,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (controller.model.status == "SUCCESS" && context.mounted) {
|
||||
context.push('/confirm');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,77 +229,32 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.black87.withAlpha(20),
|
||||
_buildTransactionDetailsCard(
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
icon: Icons.business_outlined,
|
||||
label: "Provider",
|
||||
value:
|
||||
controller
|
||||
.model
|
||||
.selectedProvider
|
||||
?.name ??
|
||||
"",
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 20,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"TRANSACTIONS DETAILS",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Provider",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller
|
||||
.model
|
||||
.selectedProvider
|
||||
?.name ??
|
||||
"",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
controller
|
||||
.model
|
||||
.selectedProvider
|
||||
?.accountFieldName ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
transactionController
|
||||
.model
|
||||
.formData
|
||||
.creditAccount,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildDetailRow(
|
||||
icon: Icons.account_balance_outlined,
|
||||
label:
|
||||
controller
|
||||
.model
|
||||
.selectedProvider
|
||||
?.accountFieldName ??
|
||||
"",
|
||||
value: transactionController
|
||||
.model
|
||||
.formData
|
||||
.creditAccount,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
InkWell(
|
||||
@@ -313,7 +300,6 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
const SizedBox(height: 7),
|
||||
_buildAmountField(),
|
||||
const SizedBox(height: 20),
|
||||
// _buildPayButton(controller),
|
||||
...controller.model.paymentProcessors.map(
|
||||
(e) => _buildPaymentProcessorItem(e, context),
|
||||
),
|
||||
@@ -343,9 +329,18 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Debit Phone Number',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Debit Phone Number',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
const Text(
|
||||
'(to be charged or notified)',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!kIsWeb)
|
||||
TextButton(
|
||||
@@ -372,7 +367,9 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
width: 100,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -427,7 +424,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.2),
|
||||
).colorScheme.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -435,7 +432,6 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter a phone number';
|
||||
}
|
||||
// Remove any non-digit characters for validation
|
||||
String digitsOnly = value.replaceAll(RegExp(r'[^\d]'), '');
|
||||
if (digitsOnly.length < 7) {
|
||||
return 'Phone number must be at least 7 digits';
|
||||
@@ -479,7 +475,9 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -526,7 +524,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.2),
|
||||
).colorScheme.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -558,7 +556,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.2),
|
||||
).colorScheme.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -586,7 +584,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
enabled: controller.model.isLoading,
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: controller.model.selectedProduct?.uid,
|
||||
initialValue: controller.model.selectedProduct?.uid,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
decoration: InputDecoration(
|
||||
@@ -633,6 +631,84 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransactionDetailsCard({required List<Widget> children}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200, width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.receipt_long_outlined,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"TRANSACTION DETAILS",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
letterSpacing: 1.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
const SizedBox(height: 16),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Colors.grey.shade500),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentProcessorItem(PaymentProcessor e, BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -667,34 +743,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
if (controller.model.isLoading) return;
|
||||
|
||||
controller.updateAmount(_amountController.text);
|
||||
if (!controller.model.selectedProvider!.requiresAmount) {
|
||||
controller.updateAmount(
|
||||
controller.model.selectedProduct!.defaultAmount
|
||||
.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// Update phone number with country code
|
||||
String fullPhoneNumber =
|
||||
selectedCountryCode + _phoneController.text;
|
||||
controller.updatePhone(fullPhoneNumber);
|
||||
|
||||
if (showAdditionalRecipientDetails) {
|
||||
controller.updateAdditionalRecipientDetails(
|
||||
_nameController.text,
|
||||
_emailController.text,
|
||||
);
|
||||
}
|
||||
controller.updateSelectedPaymentProcessor(e);
|
||||
transactionController.updateSelectedPaymentProcessor(e);
|
||||
|
||||
await controller.doTransaction();
|
||||
|
||||
if (controller.model.status == "SUCCESS" &&
|
||||
context.mounted) {
|
||||
context.push('/confirm');
|
||||
}
|
||||
await _submitPayment(e, context);
|
||||
}
|
||||
},
|
||||
child: ListTile(
|
||||
|
||||
Reference in New Issue
Block a user