Frontend first draft

This commit is contained in:
Arseni
2025-11-13 15:06:15 +03:00
parent e47f343afb
commit ddb54ddfdc
504 changed files with 25498 additions and 1 deletions

View File

@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:flutter_multi_formatter/flutter_multi_formatter.dart';
import 'package:pshared/models/payment/methods/card.dart';
import 'package:pweb/utils/text_field_styles.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class CardFormMinimal extends StatefulWidget {
final void Function(CardPaymentMethod) onChanged;
final CardPaymentMethod? initialData;
final bool isEditable;
const CardFormMinimal({
super.key,
required this.onChanged,
this.initialData,
required this.isEditable,
});
@override
State<CardFormMinimal> createState() => _CardFormMinimalState();
}
class _CardFormMinimalState extends State<CardFormMinimal> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _panController;
late TextEditingController _firstNameController;
late TextEditingController _lastNameController;
@override
void initState() {
super.initState();
_panController = TextEditingController(text: widget.initialData?.pan ?? '');
_firstNameController = TextEditingController(text: widget.initialData?.firstName ?? '');
_lastNameController = TextEditingController(text: widget.initialData?.lastName ?? '');
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
}
void _emitIfValid() {
if (_formKey.currentState?.validate() ?? false) {
widget.onChanged(
CardPaymentMethod(
pan: _panController.text.replaceAll(' ', ''),
firstName: _firstNameController.text,
lastName: _lastNameController.text,
),
);
}
}
@override
void didUpdateWidget(covariant CardFormMinimal oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialData == null && oldWidget.initialData != null) {
_panController.clear();
_firstNameController.clear();
_lastNameController.clear();
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Form(
key: _formKey,
onChanged: _emitIfValid,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
readOnly: !widget.isEditable,
controller: _panController,
decoration: getInputDecoration(context, l10n.cardNumber, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
keyboardType: TextInputType.number,
inputFormatters: [CreditCardNumberInputFormatter()],
validator: (v) => (v == null || v.replaceAll(' ', '').length < 12) ? l10n.enterCardNumber : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _firstNameController,
decoration: getInputDecoration(context, l10n.firstName, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (v) => (v == null || v.isEmpty) ? l10n.enterFirstName : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _lastNameController,
decoration: getInputDecoration(context, l10n.lastName, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (v) => (v == null || v.isEmpty) ? l10n.enterLastName : null,
),
],
),
);
}
@override
void dispose() {
_panController.dispose();
_firstNameController.dispose();
_lastNameController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/iban.dart';
import 'package:pweb/utils/text_field_styles.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class IbanForm extends StatefulWidget {
final void Function(IbanPaymentMethod) onChanged;
final IbanPaymentMethod? initialData;
final bool isEditable;
const IbanForm({
super.key,
required this.onChanged,
this.initialData,
required this.isEditable,
});
@override
State<IbanForm> createState() => _IbanFormState();
}
class _IbanFormState extends State<IbanForm> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _ibanController;
late TextEditingController _accountHolderController;
late TextEditingController _bicController;
late TextEditingController _bankNameController;
@override
void initState() {
super.initState();
_ibanController = TextEditingController(text: widget.initialData?.iban ?? '');
_accountHolderController = TextEditingController(text: widget.initialData?.accountHolder ?? '');
_bicController = TextEditingController(text: widget.initialData?.bic ?? '');
_bankNameController = TextEditingController(text: widget.initialData?.bankName ?? '');
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
}
void _emitIfValid() {
if (_formKey.currentState?.validate() ?? false) {
widget.onChanged(
IbanPaymentMethod(
iban: _ibanController.text,
accountHolder: _accountHolderController.text,
bic: _bicController.text,
bankName: _bankNameController.text,
),
);
}
}
@override
void didUpdateWidget(covariant IbanForm oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialData == null && oldWidget.initialData != null) {
_ibanController.clear();
_accountHolderController.clear();
_bicController.clear();
_bankNameController.clear();
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Form(
key: _formKey,
onChanged: _emitIfValid,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
readOnly: !widget.isEditable,
controller: _ibanController,
decoration: getInputDecoration(context, l10n.iban, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterIban : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _accountHolderController,
decoration: getInputDecoration(context, l10n.accountHolder, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterAccountHolder : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _bicController,
decoration: getInputDecoration(context, l10n.bic, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterBic : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _bankNameController,
decoration: getInputDecoration(context, l10n.bankName, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterBankName : null,
),
],
),
);
}
@override
void dispose() {
_ibanController.dispose();
_accountHolderController.dispose();
_bicController.dispose();
_bankNameController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
import 'package:pweb/utils/payment/label.dart';
class PaymentMethodTypeSelector extends StatelessWidget {
final PaymentType? value;
final ValueChanged<PaymentType?> onChanged;
const PaymentMethodTypeSelector({
super.key,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return DropdownButtonFormField<PaymentType>(
value: value,
decoration: InputDecoration(labelText: l10n.paymentType),
items: PaymentType.values.map((type) {
final label = getPaymentTypeLabel(context, type);
return DropdownMenuItem(value: type, child: Text(label));
}).toList(),
onChanged: onChanged,
validator: (val) => val == null ? l10n.selectPaymentType : null,
);
}
}

View File

@@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/russian_bank.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
import 'package:pweb/utils/text_field_styles.dart';
class RussianBankForm extends StatefulWidget {
final void Function(RussianBankAccountPaymentMethod) onChanged;
final RussianBankAccountPaymentMethod? initialData;
final bool isEditable;
const RussianBankForm({
super.key,
required this.onChanged,
this.initialData,
required this.isEditable,
});
@override
State<RussianBankForm> createState() => _RussianBankFormState();
}
class _RussianBankFormState extends State<RussianBankForm> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _recipientNameController;
late final TextEditingController _innController;
late final TextEditingController _kppController;
late final TextEditingController _bankNameController;
late final TextEditingController _bikController;
late final TextEditingController _accountNumberController;
late final TextEditingController _correspondentAccountController;
@override
void initState() {
super.initState();
_recipientNameController = TextEditingController(text: widget.initialData?.recipientName ?? '');
_innController = TextEditingController(text: widget.initialData?.inn ?? '');
_kppController = TextEditingController(text: widget.initialData?.kpp ?? '');
_bankNameController = TextEditingController(text: widget.initialData?.bankName ?? '');
_bikController = TextEditingController(text: widget.initialData?.bik ?? '');
_accountNumberController = TextEditingController(text: widget.initialData?.accountNumber ?? '');
_correspondentAccountController = TextEditingController(text: widget.initialData?.correspondentAccount ?? '');
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
}
void _emitIfValid() {
if (_formKey.currentState?.validate() ?? false) {
widget.onChanged(
RussianBankAccountPaymentMethod(
recipientName: _recipientNameController.text,
inn: _innController.text,
kpp: _kppController.text,
bankName: _bankNameController.text,
bik: _bikController.text,
accountNumber: _accountNumberController.text,
correspondentAccount: _correspondentAccountController.text,
),
);
}
}
@override
void didUpdateWidget(covariant RussianBankForm oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialData == null && oldWidget.initialData != null) {
_recipientNameController.clear();
_innController.clear();
_kppController.clear();
_bankNameController.clear();
_bikController.clear();
_accountNumberController.clear();
_correspondentAccountController.clear();
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Form(
key: _formKey,
onChanged: _emitIfValid,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
readOnly: !widget.isEditable,
controller: _recipientNameController,
decoration: getInputDecoration(context, l10n.recipientName, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterRecipientName : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _innController,
decoration: getInputDecoration(context, l10n.inn, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterInn : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _kppController,
decoration: getInputDecoration(context, l10n.kpp, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterKpp : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _bankNameController,
decoration: getInputDecoration(context, l10n.bankName, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterBankName : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _bikController,
decoration: getInputDecoration(context, l10n.bik, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterBik : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _accountNumberController,
decoration: getInputDecoration(context, l10n.accountNumber, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterAccountNumber : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _correspondentAccountController,
decoration: getInputDecoration(context, l10n.correspondentAccount, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.isEmpty) ? l10n.enterCorrespondentAccount : null,
),
],
),
);
}
@override
void dispose() {
_recipientNameController.dispose();
_innController.dispose();
_kppController.dispose();
_bankNameController.dispose();
_bikController.dispose();
_accountNumberController.dispose();
_correspondentAccountController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/wallet.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
import 'package:pweb/utils/text_field_styles.dart';
class WalletForm extends StatefulWidget {
final void Function(WalletPaymentMethod) onChanged;
final WalletPaymentMethod? initialData;
final bool isEditable;
const WalletForm({
super.key,
required this.onChanged,
this.initialData,
required this.isEditable,
});
@override
State<WalletForm> createState() => _WalletFormState();
}
class _WalletFormState extends State<WalletForm> {
late TextEditingController _walletIdController;
@override
void initState() {
super.initState();
_walletIdController = TextEditingController(text: widget.initialData?.walletId);
}
void _emit() {
if (_walletIdController.text.isNotEmpty) {
widget.onChanged(WalletPaymentMethod(walletId: _walletIdController.text));
} else {
}
}
@override
void didUpdateWidget(covariant WalletForm oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialData == null && oldWidget.initialData != null) {
_walletIdController.clear();
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return TextFormField(
readOnly: !widget.isEditable,
controller: _walletIdController,
decoration: getInputDecoration(context, l10n.walletId, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
onChanged: (_) => _emit(),
validator: (val) => (val?.isEmpty ?? true) ? l10n.enterWalletId : null,
);
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/pages/payment_methods/add/method_selector.dart';
import 'package:pweb/pages/payment_methods/form.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class AddPaymentMethodDialog extends StatefulWidget {
const AddPaymentMethodDialog({super.key});
@override
State<AddPaymentMethodDialog> createState() => _AddPaymentMethodDialogState();
}
class _AddPaymentMethodDialogState extends State<AddPaymentMethodDialog> {
final _formKey = GlobalKey<FormState>();
PaymentType? _selectedType;
// Holds current result from the selected form
Object? _currentMethod;
void _submit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
if (_currentMethod case final Object method) {
Navigator.of(context).pop(method);
}
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return AlertDialog(
title: Text(l10n.addPaymentMethod),
content: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
PaymentMethodTypeSelector(
value: _selectedType,
onChanged: (val) => setState(() {
_selectedType = val;
_currentMethod = null;
}),
),
const SizedBox(height: 16),
if (_selectedType != null)
PaymentMethodForm(
selectedType: _selectedType,
onChanged: (val) => _currentMethod = val,
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.cancel),
),
ElevatedButton(
onPressed: _submit,
child: Text(l10n.add),
),
],
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
Future<bool> showDeleteConfirmationDialog(BuildContext context) async {
final l10n = AppLocalizations.of(context)!;
return await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text(l10n.delete),
content: Text(l10n.deletePaymentConfirmation),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(l10n.cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(l10n.delete),
),
],
),
) ?? false;
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/iban.dart';
import 'package:pshared/models/payment/methods/russian_bank.dart';
import 'package:pshared/models/payment/methods/wallet.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/pages/payment_methods/add/card.dart';
import 'package:pweb/pages/payment_methods/add/iban.dart';
import 'package:pweb/pages/payment_methods/add/russian_bank.dart';
import 'package:pweb/pages/payment_methods/add/wallet.dart';
class PaymentMethodForm extends StatelessWidget {
final PaymentType? selectedType;
final ValueChanged<Object?> onChanged;
final Object? initialData;
final bool isEditable;
const PaymentMethodForm({
super.key,
required this.selectedType,
required this.onChanged,
this.initialData,
this.isEditable = true,
});
@override
Widget build(BuildContext context) {
return switch (selectedType) {
PaymentType.card => CardFormMinimal(
onChanged: onChanged,
initialData: initialData as CardPaymentMethod?,
isEditable: isEditable,
),
PaymentType.iban => IbanForm(
onChanged: onChanged,
initialData: initialData as IbanPaymentMethod?,
isEditable: isEditable,
),
PaymentType.wallet => WalletForm(
onChanged: onChanged,
initialData: initialData as WalletPaymentMethod?,
isEditable: isEditable,
),
PaymentType.bankAccount => RussianBankForm(
onChanged: onChanged,
initialData: initialData as RussianBankAccountPaymentMethod?,
isEditable: isEditable,
),
_ => const SizedBox.shrink(),
};
}
}

View File

@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
IconData iconForPaymentType(PaymentType type) {
switch (type) {
case PaymentType.bankAccount:
return Icons.account_balance;
case PaymentType.iban:
return Icons.language;
case PaymentType.wallet:
return Icons.account_balance_wallet;
case PaymentType.card:
return Icons.credit_card;
}
}

View File

@@ -0,0 +1,232 @@
import 'package:amplitude_flutter/amplitude.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/dashboard/payouts/payment_form.dart';
import 'package:pweb/pages/dashboard/payouts/single/form/details.dart';
import 'package:pweb/pages/dashboard/payouts/single/form/header.dart';
import 'package:pweb/providers/payment_methods.dart';
import 'package:pweb/providers/recipient.dart';
import 'package:pweb/services/amplitude.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/utils/payment/dropdown.dart';
import 'package:pweb/utils/payment/selector_type.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
//TODO: decide whether to make AppDimensions universal for the whole app or leave it as it is - unique for this page alone
class PaymentPage extends StatefulWidget {
final PaymentType? type;
final ValueChanged<Recipient?>? onBack;
const PaymentPage({super.key, this.type, this.onBack});
@override
State<PaymentPage> createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> {
late Map<PaymentType, Object> _availableTypes;
late PaymentType _selectedType;
bool _isFormVisible = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final recipientProvider = context.watch<RecipientProvider>();
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipient = recipientProvider.selectedRecipient;
// Initialize available types based on whether we have a recipient
if (recipient != null) {
// We have a recipient - use their payment methods
_availableTypes = {
if (recipient.card != null) PaymentType.card: recipient.card!,
if (recipient.iban != null) PaymentType.iban: recipient.iban!,
if (recipient.wallet != null) PaymentType.wallet: recipient.wallet!,
if (recipient.bank != null) PaymentType.bankAccount: recipient.bank!,
};
// Set selected type if it's available, otherwise use first available type
if (_availableTypes.containsKey(_selectedType)) {
// Keep current selection if valid
} else if (_availableTypes.isNotEmpty) {
_selectedType = _availableTypes.keys.first;
} else {
// Fallback if recipient has no payment methods
_selectedType = PaymentType.bankAccount;
}
} else {
// No recipient - we're creating a new payment from scratch
_availableTypes = {};
_selectedType = widget.type ?? PaymentType.bankAccount;
_isFormVisible = true; // Always show form when creating new payment
}
// Load payment methods if not already loaded
if (methodsProvider.methods.isEmpty && !methodsProvider.isLoading) {
WidgetsBinding.instance.addPostFrameCallback((_) {
methodsProvider.loadMethods();
});
}
}
@override
void initState() {
super.initState();
// Initial values
_availableTypes = {};
_selectedType = widget.type ?? PaymentType.bankAccount;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dimensions = AppDimensions();
final recipientProvider = context.watch<RecipientProvider>();
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipient = recipientProvider.selectedRecipient;
// Show loading state for payment methods
if (methodsProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
// Show error state for payment methods
if (methodsProvider.error != null) {
return Center(
child: Text('Error: ${methodsProvider.error}'),
);
}
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth),
child: Material(
elevation: dimensions.elevationSmall,
borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium),
color: theme.colorScheme.onSecondary,
child: Padding(
padding: EdgeInsets.all(dimensions.paddingLarge),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back button
Align(
alignment: Alignment.topLeft,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
widget.onBack?.call(recipient);
},
),
),
SizedBox(height: dimensions.paddingSmall),
// Header
Row(
children: [
Icon(
Icons.send_rounded,
color: theme.colorScheme.primary,
size: dimensions.iconSizeLarge
),
SizedBox(width: dimensions.spacingSmall),
Text(
AppLocalizations.of(context)!.sendTo,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold
),
),
],
),
SizedBox(height: dimensions.paddingXXLarge),
// Payment method dropdown (user's payment methods)
PaymentMethodDropdown(
methods: methodsProvider.methods,
initialValue: methodsProvider.selectedMethod,
onChanged: (method) {
methodsProvider.selectMethod(method);
},
),
SizedBox(height: dimensions.paddingXLarge),
// Recipient section (only show if we have a recipient)
if (recipient != null) ...[
RecipientHeader(recipient: recipient),
SizedBox(height: dimensions.paddingMedium),
// Payment type selector (recipient's payment methods)
if (_availableTypes.isNotEmpty)
PaymentTypeSelector(
availableTypes: _availableTypes,
selectedType: _selectedType,
onSelected: (type) => setState(() => _selectedType = type),
),
SizedBox(height: dimensions.paddingMedium),
],
// Payment details section
PaymentDetailsSection(
isFormVisible: recipient == null || _isFormVisible,
onToggle: recipient != null
? () => setState(() => _isFormVisible = !_isFormVisible)
: null, // No toggle when creating new payment
selectedType: _selectedType,
data: _availableTypes[_selectedType],
isEditable: recipient == null,
),
const PaymentFormWidget(),
SizedBox(height: dimensions.paddingXXXLarge),
Center(
child: SizedBox(
width: dimensions.buttonWidth,
height: dimensions.buttonHeight,
child: InkWell(
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
onTap: () =>
// TODO: Handle Payment logic
AmplitudeService.pageOpened(PayoutDestination.payment), //TODO: replace with payment event
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
),
child: Center(
child: Text(
AppLocalizations.of(context)!.send,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSecondary,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
),
SizedBox(height: dimensions.paddingLarge),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/payment_methods/icon.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentMethodTile extends StatelessWidget {
const PaymentMethodTile({
super.key,
required this.method,
required this.index,
required this.makeMain,
required this.toggleEnabled,
required this.edit,
required this.delete,
});
final PaymentMethod method;
final int index;
final VoidCallback makeMain;
final ValueChanged<bool> toggleEnabled;
final VoidCallback edit;
final VoidCallback delete;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
return Opacity(
opacity: method.isEnabled ? 1 : 0.5,
child: Card(
margin: const EdgeInsets.symmetric(vertical: 4),
elevation: 0,
child: ListTile(
key: ValueKey(method.id),
leading: Icon(iconForPaymentType(method.type)),
onTap: makeMain,
title: Row(
children: [
Expanded(child: Text(method.label)),
Text(
method.details,
style: theme.textTheme.bodySmall,
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildMakeMainButton(context),
_buildEnabledSwitch(),
_buildPopupMenu(l10n),
],
),
),
),
);
}
Widget _buildMakeMainButton(BuildContext context) {
final theme = Theme.of(context);
return IconButton(
tooltip: 'Make main',
icon: Icon(
method.isMain ? Icons.star : Icons.star_outline,
color: method.isMain ? theme.colorScheme.primary : null,
),
onPressed: makeMain,
);
}
Widget _buildEnabledSwitch() {
return Switch.adaptive(
value: method.isEnabled,
onChanged: toggleEnabled,
);
}
Widget _buildPopupMenu(AppLocalizations l10n) {
return PopupMenuButton<String>(
tooltip: l10n.moreActions,
onSelected: (value) {
switch (value) {
case 'edit':
edit();
break;
case 'delete':
delete();
break;
}
},
itemBuilder: (_) => [
PopupMenuItem(value: 'edit', child: Text(l10n.edit)),
PopupMenuItem(value: 'delete', child: Text(l10n.delete)),
],
);
}
}