Frontend first draft
This commit is contained in:
112
frontend/pweb/lib/pages/payment_methods/add/card.dart
Normal file
112
frontend/pweb/lib/pages/payment_methods/add/card.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
122
frontend/pweb/lib/pages/payment_methods/add/iban.dart
Normal file
122
frontend/pweb/lib/pages/payment_methods/add/iban.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
162
frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart
Normal file
162
frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
62
frontend/pweb/lib/pages/payment_methods/add/wallet.dart
Normal file
62
frontend/pweb/lib/pages/payment_methods/add/wallet.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
76
frontend/pweb/lib/pages/payment_methods/add/widget.dart
Normal file
76
frontend/pweb/lib/pages/payment_methods/add/widget.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
55
frontend/pweb/lib/pages/payment_methods/form.dart
Normal file
55
frontend/pweb/lib/pages/payment_methods/form.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
17
frontend/pweb/lib/pages/payment_methods/icon.dart
Normal file
17
frontend/pweb/lib/pages/payment_methods/icon.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
232
frontend/pweb/lib/pages/payment_methods/page.dart
Normal file
232
frontend/pweb/lib/pages/payment_methods/page.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
100
frontend/pweb/lib/pages/payment_methods/title.dart
Normal file
100
frontend/pweb/lib/pages/payment_methods/title.dart
Normal 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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user