793 lines
28 KiB
Dart
793 lines
28 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:qpay/models/responsive_policy.dart';
|
|
import 'package:qpay/screens/pay/pay_controller.dart';
|
|
import 'package:qpay/screens/transaction_controller.dart';
|
|
import 'package:qpay/widgets/app_snack_bar.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:skeletonizer/skeletonizer.dart';
|
|
|
|
class PayScreen extends StatefulWidget {
|
|
const PayScreen({super.key});
|
|
|
|
@override
|
|
State<PayScreen> createState() => _PayScreenState();
|
|
}
|
|
|
|
class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _fadeAnimation;
|
|
late Animation<Offset> _slideAnimation;
|
|
late PayController controller;
|
|
late TransactionController transactionController;
|
|
late SharedPreferences prefs;
|
|
|
|
final List<Animation<Offset>> _animations = [];
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _amountController = TextEditingController();
|
|
final _nameController = TextEditingController();
|
|
final _emailController = TextEditingController();
|
|
final _phoneController = TextEditingController();
|
|
bool showAdditionalRecipientDetails = false;
|
|
String selectedCountryCode = '263'; // Default to ZW
|
|
|
|
// Common country codes with flags and names
|
|
final List<Map<String, String>> countryCodes = [
|
|
{'code': '263', 'name': 'ZW', 'flag': '🇿🇼'},
|
|
{'code': '27', 'name': 'ZA', 'flag': '🇿🇦'},
|
|
{'code': '1', 'name': 'US', 'flag': '🇺🇸'},
|
|
{'code': '44', 'name': 'UK', 'flag': '🇬🇧'},
|
|
{'code': '61', 'name': 'AU', 'flag': '🇦🇺'},
|
|
{'code': '91', 'name': 'IN', 'flag': '🇮🇳'},
|
|
{'code': '86', 'name': 'CN', 'flag': '🇨🇳'},
|
|
{'code': '81', 'name': 'JP', 'flag': '🇯🇵'},
|
|
{'code': '49', 'name': 'DE', 'flag': '🇩🇪'},
|
|
{'code': '33', 'name': 'FR', 'flag': '🇫🇷'},
|
|
{'code': '39', 'name': 'IT', 'flag': '🇮🇹'},
|
|
{'code': '34', 'name': 'ES', 'flag': '🇪🇸'},
|
|
{'code': '7', 'name': 'RU', 'flag': '🇷🇺'},
|
|
{'code': '55', 'name': 'BR', 'flag': '🇧🇷'},
|
|
{'code': '52', 'name': 'MX', 'flag': '🇲🇽'},
|
|
{'code': '971', 'name': 'AE', 'flag': '🇦🇪'},
|
|
{'code': '966', 'name': 'SA', 'flag': '🇸🇦'},
|
|
{'code': '234', 'name': 'NG', 'flag': '🇳🇬'},
|
|
{'code': '254', 'name': 'KE', 'flag': '🇰🇪'},
|
|
{'code': '233', 'name': 'GH', 'flag': '🇬🇭'},
|
|
{'code': '256', 'name': 'UG', 'flag': '🇺🇬'},
|
|
];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
controller = PayController(context);
|
|
controller.getPaymentProcessors();
|
|
|
|
controller.getProducts(controller.model.selectedProvider?.id ?? "");
|
|
transactionController = Provider.of<TransactionController>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
|
|
_setupData();
|
|
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 800),
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
CurvedAnimation(
|
|
parent: _controller,
|
|
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
|
|
),
|
|
);
|
|
|
|
_slideAnimation =
|
|
Tween<Offset>(begin: const Offset(0, 0.1), end: Offset.zero).animate(
|
|
CurvedAnimation(
|
|
parent: _controller,
|
|
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
|
|
),
|
|
);
|
|
|
|
List<int> buttonIndices = [0, 1];
|
|
for (int i = 0; i < buttonIndices.length; i++) {
|
|
_animations.add(
|
|
Tween<Offset>(begin: Offset(-0.15, 0), end: Offset.zero).animate(
|
|
CurvedAnimation(
|
|
parent: _controller,
|
|
curve: Interval(0.055 * i, 1.0, curve: Curves.easeOut),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
_controller.forward();
|
|
}
|
|
|
|
_setupData() async {
|
|
prefs = await SharedPreferences.getInstance();
|
|
// check logged in phone number and prefill if exists
|
|
if (prefs.containsKey('phone')) {
|
|
updatePhoneNumber(prefs.getString('phone')!);
|
|
}
|
|
// or else check if there's a phone number from a previous transaction and prefill that
|
|
else if (transactionController.model.formData.debitPhone != null) {
|
|
updatePhoneNumber(transactionController.model.formData.debitPhone!);
|
|
}
|
|
|
|
_nameController.text =
|
|
transactionController.model.formData.creditName ?? '';
|
|
_emailController.text =
|
|
transactionController.model.formData.creditEmail ?? '';
|
|
_amountController.text = transactionController.model.formData.amount;
|
|
}
|
|
|
|
Future<void> updatePhoneNumber(String newNumber) async {
|
|
newNumber = newNumber.replaceAll(RegExp(r'[^0-9]'), '');
|
|
for (var country in countryCodes) {
|
|
if (newNumber.startsWith(country['code']!)) {
|
|
setState(() {
|
|
selectedCountryCode = country['code']!;
|
|
});
|
|
_phoneController.text = newNumber.substring(country['code']!.length);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
String getCountryName(String code) {
|
|
return countryCodes.firstWhere((c) => c['code'] == code)['name']!;
|
|
}
|
|
|
|
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);
|
|
|
|
controller.model.region = getCountryName(selectedCountryCode);
|
|
|
|
final apiResponse = await controller.doTransaction();
|
|
|
|
if (!apiResponse.isSuccess) {
|
|
if (context.mounted) {
|
|
AppSnackBar.showError(
|
|
context,
|
|
controller.model.errorMessage ??
|
|
apiResponse.error ??
|
|
"Transaction failed",
|
|
);
|
|
}
|
|
} else if (controller.model.status == "SUCCESS" && context.mounted) {
|
|
context.push('/confirm');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_amountController.dispose();
|
|
_nameController.dispose();
|
|
_emailController.dispose();
|
|
_phoneController.dispose();
|
|
controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: context.canPop()
|
|
? IconButton(
|
|
onPressed: () {
|
|
context.pop();
|
|
},
|
|
icon: const Icon(Icons.arrow_back),
|
|
)
|
|
: SizedBox.shrink(),
|
|
title: const Text(
|
|
'Make Payment',
|
|
style: TextStyle(color: Colors.black87),
|
|
),
|
|
),
|
|
body: ListenableBuilder(
|
|
listenable: controller,
|
|
builder: (context, child) {
|
|
return SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: SlideTransition(
|
|
position: _slideAnimation,
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Center(
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
return SizedBox(
|
|
width: double.parse(
|
|
constraints.maxWidth > ResponsivePolicy.md
|
|
? ResponsivePolicy.md.toString()
|
|
: constraints.maxWidth.toString(),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildTransactionDetailsCard(
|
|
children: [
|
|
_buildDetailRow(
|
|
icon: Icons.business_outlined,
|
|
label: "Provider",
|
|
value:
|
|
controller
|
|
.model
|
|
.selectedProvider
|
|
?.name ??
|
|
"",
|
|
),
|
|
_buildDetailRow(
|
|
icon: Icons.account_balance_outlined,
|
|
label:
|
|
controller
|
|
.model
|
|
.selectedProvider
|
|
?.accountFieldName ??
|
|
"",
|
|
value: transactionController
|
|
.model
|
|
.formData
|
|
.creditAccount,
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 20),
|
|
InkWell(
|
|
customBorder: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
onTap: () {
|
|
setState(() {
|
|
showAdditionalRecipientDetails =
|
|
!showAdditionalRecipientDetails;
|
|
});
|
|
},
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
IconButton(
|
|
onPressed: () {},
|
|
icon: Icon(
|
|
showAdditionalRecipientDetails
|
|
? Icons.arrow_drop_up
|
|
: Icons.arrow_drop_down,
|
|
),
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary,
|
|
),
|
|
Text(
|
|
showAdditionalRecipientDetails
|
|
? "Hide additional recipient details"
|
|
: "Show additional recipient details",
|
|
style: TextStyle(fontSize: 16),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (showAdditionalRecipientDetails)
|
|
_buildAdditionalRecipientDetails(),
|
|
const SizedBox(height: 7),
|
|
if (controller.model.products.isNotEmpty ||
|
|
controller.model.isLoading)
|
|
_buildBillProductSelector(controller),
|
|
const SizedBox(height: 7),
|
|
_buildPhoneField(),
|
|
const SizedBox(height: 7),
|
|
_buildAmountField(),
|
|
const SizedBox(height: 20),
|
|
...controller.model.paymentProcessors.map(
|
|
(e) => _buildPaymentProcessorItem(e, context),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPhoneField() {
|
|
if (controller.model.selectedProvider?.requiresPhone == false) {
|
|
return SizedBox.shrink();
|
|
}
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
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(
|
|
onPressed: () async {
|
|
if (await FlutterContacts.requestPermission()) {
|
|
final contact = await FlutterContacts.openExternalPick();
|
|
if (contact != null) {
|
|
updatePhoneNumber(contact.phones.first.normalizedNumber);
|
|
}
|
|
}
|
|
},
|
|
child: Text(
|
|
"Fetch from contacts",
|
|
style: TextStyle(fontSize: 12, color: Colors.red),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 5),
|
|
Row(
|
|
children: [
|
|
// Country code dropdown
|
|
Container(
|
|
width: 100,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.2),
|
|
),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButton<String>(
|
|
value: selectedCountryCode,
|
|
isExpanded: true,
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
items: countryCodes.map((country) {
|
|
return DropdownMenuItem<String>(
|
|
value: country['code'],
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(country['flag']!),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
country['code']!,
|
|
style: const TextStyle(fontSize: 14),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
onChanged: (String? newValue) {
|
|
if (newValue != null) {
|
|
setState(() {
|
|
selectedCountryCode = newValue;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Phone number field
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: _phoneController,
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
decoration: InputDecoration(
|
|
hintText: 'Enter phone number',
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.2),
|
|
),
|
|
),
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Please enter a phone number';
|
|
}
|
|
String digitsOnly = value.replaceAll(RegExp(r'[^\d]'), '');
|
|
if (digitsOnly.length < 7) {
|
|
return 'Phone number must be at least 7 digits';
|
|
}
|
|
if (digitsOnly.length > 15) {
|
|
return 'Phone number is too long';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildAmountField() {
|
|
if (controller.model.selectedProvider?.requiresAmount == false) {
|
|
return SizedBox.shrink();
|
|
}
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Amount',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
|
),
|
|
const SizedBox(height: 5),
|
|
TextFormField(
|
|
controller: _amountController,
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
decoration: InputDecoration(
|
|
hintText: 'Enter amount',
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.2),
|
|
),
|
|
),
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Please enter an amount';
|
|
}
|
|
if (double.tryParse(value) == null) {
|
|
return 'Please enter a valid amount';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildAdditionalRecipientDetails() {
|
|
return SlideTransition(
|
|
position: _slideAnimation,
|
|
child: Column(
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Name',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
|
),
|
|
const SizedBox(height: 5),
|
|
TextFormField(
|
|
controller: _nameController,
|
|
keyboardType: TextInputType.text,
|
|
decoration: InputDecoration(
|
|
hintText: 'Enter recipient\'s name',
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.2),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Email',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
|
),
|
|
const SizedBox(height: 5),
|
|
TextFormField(
|
|
controller: _emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: InputDecoration(
|
|
hintText: 'Enter recipient\'s email',
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.2),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Divider(color: Theme.of(context).colorScheme.primary),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBillProductSelector(PayController controller) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(height: 20),
|
|
const Text(
|
|
'Provider Product',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
|
),
|
|
const SizedBox(height: 5),
|
|
Skeletonizer(
|
|
enabled: controller.model.isLoading,
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButtonFormField<String>(
|
|
initialValue: controller.model.selectedProduct?.uid,
|
|
isExpanded: true,
|
|
icon: const Icon(Icons.arrow_drop_down),
|
|
decoration: InputDecoration(
|
|
hintText: 'Select Product',
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.2),
|
|
),
|
|
),
|
|
),
|
|
items: [
|
|
DropdownMenuItem(child: Text("Select Product")),
|
|
...controller.model.products.map(
|
|
(e) => DropdownMenuItem<String>(
|
|
value: e.uid,
|
|
child: Text(
|
|
'${e.displayName} - '
|
|
'${transactionController.model.formData.debitCurrency}${e.defaultAmount}',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
onChanged: (String? newValue) {
|
|
if (newValue != null) {
|
|
BillProduct product = controller.model.products.firstWhere(
|
|
(e) => e.uid == newValue,
|
|
);
|
|
controller.updateSelectedProduct(product);
|
|
}
|
|
},
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Please select a product';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
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: [
|
|
Skeletonizer(
|
|
enabled: controller.model.isLoading,
|
|
child: SlideTransition(
|
|
position:
|
|
_animations[controller.model.paymentProcessors.indexOf(e)],
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.black87.withAlpha(20)),
|
|
borderRadius: BorderRadius.circular(12),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.1),
|
|
Theme.of(
|
|
context,
|
|
).colorScheme.tertiary.withValues(alpha: 0.05),
|
|
],
|
|
stops: const [0.3, 0.7],
|
|
begin: Alignment.topRight,
|
|
end: Alignment.bottomLeft,
|
|
),
|
|
),
|
|
child: InkWell(
|
|
customBorder: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
onTap: () async {
|
|
if (_formKey.currentState!.validate()) {
|
|
if (controller.model.isLoading) return;
|
|
|
|
await _submitPayment(e, context);
|
|
}
|
|
},
|
|
child: ListTile(
|
|
leading: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Image(image: AssetImage("assets/${e.image}")),
|
|
),
|
|
title: Text(
|
|
e.name,
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
subtitle: Text(
|
|
e.description,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.normal,
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
trailing: Icon(
|
|
Icons.chevron_right,
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.secondary.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 10),
|
|
],
|
|
);
|
|
}
|
|
}
|