redesigned payment page + a lot of fixes

This commit is contained in:
Arseni
2026-02-21 21:55:20 +03:00
parent a68aa2abff
commit 0c6fa03aba
208 changed files with 4062 additions and 2217 deletions

View File

@@ -2,15 +2,14 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/controllers/organization/address_book_recipient_form.dart';
import 'package:pweb/controllers/organization/address_book_recipient_form_selection.dart';
import 'package:pweb/pages/address_book/form/view.dart';
import 'package:pweb/providers/address_book_recipient_form.dart';
import 'package:pweb/utils/payment/availability.dart';
class AddressBookRecipientFormBody extends StatefulWidget {
class AddressBookRecipientFormBody extends StatelessWidget {
final GlobalKey<FormState> formKey;
final TextEditingController nameCtrl;
final TextEditingController emailCtrl;
@@ -27,94 +26,43 @@ class AddressBookRecipientFormBody extends StatefulWidget {
required this.onBack,
});
@override
State<AddressBookRecipientFormBody> createState() => _AddressBookRecipientFormBodyState();
}
class _AddressBookRecipientFormBodyState extends State<AddressBookRecipientFormBody> {
PaymentType? _selectedType;
int? _selectedIndex;
void _reconcileSelection(AddressBookRecipientFormProvider formState) {
final types = formState.supportedTypes;
if (types.isEmpty) return;
var nextType = _selectedType;
var nextIndex = _selectedIndex;
if (nextType == null || !types.contains(nextType)) {
nextType = formState.preferredType ?? types.first;
nextIndex = null;
}
final entries = formState.methods[nextType] ?? const [];
if (entries.isEmpty) {
nextIndex = null;
} else if (nextIndex == null || nextIndex < 0 || nextIndex >= entries.length) {
nextIndex = 0;
}
if (nextType == _selectedType && nextIndex == _selectedIndex) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {
_selectedType = nextType;
_selectedIndex = nextIndex;
});
});
}
void _onMethodSelected(PaymentType type, int index) {
setState(() {
_selectedType = type;
_selectedIndex = index;
});
}
void _onMethodAdd(AddressBookRecipientFormProvider formState, PaymentType type) {
final newIndex = formState.addMethod(type);
setState(() {
_selectedType = type;
_selectedIndex = newIndex;
});
}
void _onMethodRemove(AddressBookRecipientFormProvider formState, int index) {
final type = _selectedType ?? formState.supportedTypes.first;
formState.removeMethod(type, index);
}
void _onMethodChanged(
AddressBookRecipientFormProvider formState,
int index,
PaymentMethodData data,
) {
final type = _selectedType ?? formState.supportedTypes.first;
formState.updateMethod(type, index, data);
}
@override
Widget build(BuildContext context) {
final formState = context.watch<AddressBookRecipientFormProvider>();
_reconcileSelection(formState);
final formState = Provider.of<AddressBookRecipientFormProvider>(context, listen: false);
final controller = Provider.of<AddressBookRecipientFormController>(context);
final selection =
Provider.of<AddressBookRecipientFormSelectionController>(context);
final selectedType = _selectedType ?? formState.supportedTypes.first;
if (controller.supportedTypes.isEmpty) {
return const SizedBox.shrink();
}
final selectedType = selection.selectedType ?? controller.supportedTypes.first;
return FormView(
formKey: widget.formKey,
nameCtrl: widget.nameCtrl,
emailCtrl: widget.emailCtrl,
types: formState.supportedTypes,
formKey: formKey,
nameCtrl: nameCtrl,
emailCtrl: emailCtrl,
types: controller.supportedTypes,
selectedType: selectedType,
selectedIndex: _selectedIndex,
methods: formState.methods,
onMethodSelected: _onMethodSelected,
onMethodAdd: (type) => _onMethodAdd(formState, type),
selectedIndex: selection.selectedIndex,
methods: controller.methods,
onMethodSelected: selection.select,
onMethodAdd: (type) {
final newIndex = controller.addMethod(type);
selection.selectAfterAdd(type, newIndex);
},
disabledTypes: disabledPaymentTypes,
onMethodRemove: (index) => _onMethodRemove(formState, index),
onMethodChanged: (index, data) => _onMethodChanged(formState, index, data),
onSave: () => widget.onSave(formState),
isEditing: widget.isEditing,
onBack: widget.onBack,
onMethodRemove: (index) {
final type = selection.selectedType ?? controller.supportedTypes.first;
controller.removeMethod(type, index);
},
onMethodChanged: (index, data) {
final type = selection.selectedType ?? controller.supportedTypes.first;
controller.updateMethod(type, index, data);
},
onSave: () => onSave(formState),
isEditing: isEditing,
onBack: onBack,
);
}
}

View File

@@ -7,12 +7,11 @@ import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/methods_cache.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/controllers/organization/address_book_recipient_form.dart';
import 'package:pweb/controllers/organization/address_book_recipient_form_selection.dart';
import 'package:pweb/pages/address_book/form/body.dart';
import 'package:pweb/providers/address_book_recipient_form.dart';
import 'package:pweb/utils/payment/availability.dart';
import 'package:pweb/utils/payment/label.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class AddressBookRecipientForm extends StatefulWidget {
@@ -29,6 +28,8 @@ class _AddressBookRecipientFormState extends State<AddressBookRecipientForm> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameCtrl;
late TextEditingController _emailCtrl;
late final String _initialName;
late final String _initialEmail;
static const List<PaymentType> _supportedTypes = visiblePaymentTypes;
@@ -36,61 +37,79 @@ class _AddressBookRecipientFormState extends State<AddressBookRecipientForm> {
void initState() {
super.initState();
final r = widget.recipient;
_nameCtrl = TextEditingController(text: r?.name ?? '');
_emailCtrl = TextEditingController(text: r?.email ?? '');
}
Map<PaymentType, String> _methodNames(BuildContext context) => {
for (final type in _supportedTypes) type: getPaymentTypeLabel(context, type),
};
Future<void> _save(AddressBookRecipientFormProvider formState) async {
final l10n = AppLocalizations.of(context)!;
if (!_formKey.currentState!.validate() || !formState.hasAnyMethod) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.recipientFormRule)),
);
return;
}
try {
final saved = await formState.save(
name: _nameCtrl.text,
email: _emailCtrl.text,
methodNames: _methodNames(context),
);
widget.onSaved?.call(saved);
} catch (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.notificationError(l10n.noErrorInformation))),
);
}
_initialName = r?.name ?? '';
_initialEmail = r?.email ?? '';
_nameCtrl = TextEditingController(text: _initialName);
_emailCtrl = TextEditingController(text: _initialEmail);
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider2<
RecipientsProvider,
RecipientMethodsCacheProvider,
AddressBookRecipientFormProvider
>(
create: (_) => AddressBookRecipientFormProvider(
recipient: widget.recipient,
supportedTypes: _supportedTypes,
),
update: (_, recipientsProvider, methodsCache, formProvider) =>
formProvider!..updateProviders(
recipientsProvider: recipientsProvider,
methodsCache: methodsCache,
return MultiProvider(
providers: [
ChangeNotifierProxyProvider2<
RecipientsProvider,
RecipientMethodsCacheProvider,
AddressBookRecipientFormProvider
>(
create: (_) => AddressBookRecipientFormProvider(
recipient: widget.recipient,
),
child: AddressBookRecipientFormBody(
formKey: _formKey,
nameCtrl: _nameCtrl,
emailCtrl: _emailCtrl,
isEditing: widget.recipient != null,
onSave: _save,
onBack: () => widget.onSaved?.call(null),
update: (_, recipientsProvider, methodsCache, formProvider) =>
formProvider!..updateProviders(
recipientsProvider: recipientsProvider,
methodsCache: methodsCache,
),
),
ChangeNotifierProxyProvider<
RecipientMethodsCacheProvider,
AddressBookRecipientFormController
>(
create: (_) => AddressBookRecipientFormController(
supportedTypes: _supportedTypes,
),
update: (_, methodsCache, controller) => controller!
..update(
recipient: widget.recipient,
methodsCache: methodsCache,
),
),
ChangeNotifierProxyProvider<
AddressBookRecipientFormController,
AddressBookRecipientFormSelectionController
>(
create: (_) => AddressBookRecipientFormSelectionController(),
update: (_, formController, selectionController) =>
selectionController!..update(formController),
),
],
child: Builder(
builder: (context) {
final formState = context.read<AddressBookRecipientFormProvider>();
final controller = context.read<AddressBookRecipientFormController>();
return AddressBookRecipientFormBody(
formKey: _formKey,
nameCtrl: _nameCtrl,
emailCtrl: _emailCtrl,
isEditing: widget.recipient != null,
onSave: (form) => controller.saveForm(
context: context,
formKey: _formKey,
formState: form,
name: _nameCtrl.text,
email: _emailCtrl.text,
onSaved: widget.onSaved,
),
onBack: () => controller.handleBack(
context: context,
formKey: _formKey,
formState: formState,
name: _nameCtrl.text,
email: _emailCtrl.text,
onSaved: widget.onSaved,
),
);
},
),
);
}

View File

@@ -12,7 +12,8 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class AddPaymentMethodButton extends StatelessWidget {
final List<PaymentType> types;
final Set<PaymentType> disabledTypes;
final ValueChanged<PaymentType> onAdd;
final ValueChanged<PaymentType>? onAdd;
final VoidCallback? onPressed;
static const double _borderRadius = 14;
static const double _iconSize = 18;
@@ -20,13 +21,18 @@ class AddPaymentMethodButton extends StatelessWidget {
static const double _menuIconSize = 18;
static const double _menuIconTextSpacing = 8;
static const double _buttonHeight = 70;
static const EdgeInsets _buttonPadding = EdgeInsets.symmetric(horizontal: 14, vertical: 10);
static const EdgeInsets _buttonPadding = EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
);
static const FontWeight _labelWeight = FontWeight.w600;
const AddPaymentMethodButton({
super.key,
required this.types,
required this.disabledTypes,
required this.onAdd,
this.onAdd,
this.onPressed,
});
@override
@@ -41,6 +47,38 @@ class AddPaymentMethodButton extends StatelessWidget {
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withValues(alpha: 0.4);
final buttonChild = Container(
height: _buttonHeight,
padding: _buttonPadding,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_borderRadius),
border: Border.all(color: borderColor),
color: theme.colorScheme.onSecondary,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, size: _iconSize, color: textColor),
const SizedBox(width: _iconTextSpacing),
Text(
l10n.addPaymentMethod,
style: theme.textTheme.titleSmall?.copyWith(
color: textColor,
fontWeight: _labelWeight,
),
),
],
),
);
final onPressed = this.onPressed;
if (onPressed != null) {
return GestureDetector(
onTap: hasEnabled ? onPressed : null,
child: buttonChild,
);
}
return PopupMenuButton<PaymentType>(
enabled: hasEnabled,
onSelected: onAdd,
@@ -67,29 +105,7 @@ class AddPaymentMethodButton extends StatelessWidget {
);
})
.toList(),
child: Container(
height: _buttonHeight,
padding: _buttonPadding,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_borderRadius),
border: Border.all(color: borderColor),
color: theme.colorScheme.onSecondary,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, size: _iconSize, color: textColor),
const SizedBox(width: _iconTextSpacing),
Text(
l10n.addPaymentMethod,
style: theme.textTheme.titleSmall?.copyWith(
color: textColor,
fontWeight: _labelWeight,
),
),
],
),
),
child: buttonChild,
);
}
}

View File

@@ -7,6 +7,8 @@ import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/pages/payment_methods/form.dart';
import 'package:pweb/pages/payment_methods/icon.dart';
import 'package:pweb/widgets/dialogs/confirmation_dialog.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -17,6 +19,8 @@ class PaymentMethodPanel extends StatelessWidget {
final List<RecipientMethodDraft> entries;
final ValueChanged<int> onRemove;
final void Function(int, PaymentMethodData) onChanged;
final ControlState editState;
final VisibilityState deleteVisibility;
final double padding;
@@ -27,6 +31,8 @@ class PaymentMethodPanel extends StatelessWidget {
required this.entries,
required this.onRemove,
required this.onChanged,
this.editState = ControlState.enabled,
this.deleteVisibility = VisibilityState.visible,
this.padding = 16,
});
@@ -79,7 +85,7 @@ class PaymentMethodPanel extends StatelessWidget {
),
),
),
if (entry != null)
if (entry != null && deleteVisibility == VisibilityState.visible)
TextButton.icon(
onPressed: () => _confirmDelete(context, () => onRemove(selectedIndex)),
icon: Icon(Icons.delete, color: theme.colorScheme.error),
@@ -96,6 +102,7 @@ class PaymentMethodPanel extends StatelessWidget {
key: ValueKey('${selectedType.name}-${entry.existing?.id ?? selectedIndex}-form'),
selectedType: selectedType,
initialData: entry.data,
isEditable: editState == ControlState.enabled,
onChanged: (data) {
if (data == null) return;
onChanged(selectedIndex, data);

View File

@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/models/payment_method_tile/availability.dart';
import 'package:pweb/models/payment_method_tile/selection.dart';
import 'package:pweb/models/payment/method_tile/availability.dart';
import 'package:pweb/models/payment/method_tile/selection.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/add_button.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/tile.dart';
@@ -15,8 +15,10 @@ class PaymentMethodSelectorRow extends StatelessWidget {
final int? selectedIndex;
final Map<PaymentType, List<RecipientMethodDraft>> methods;
final void Function(PaymentType type, int index) onSelected;
final ValueChanged<PaymentType> onAdd;
final ValueChanged<PaymentType>? onAdd;
final VoidCallback? onAddPressed;
final Set<PaymentType> disabledTypes;
final String? Function(RecipientMethodDraft entry)? detailsBuilder;
final double spacing;
final double tilePadding;
@@ -29,8 +31,10 @@ class PaymentMethodSelectorRow extends StatelessWidget {
required this.selectedIndex,
required this.methods,
required this.onSelected,
required this.onAdd,
this.onAdd,
this.onAddPressed,
this.disabledTypes = const {},
this.detailsBuilder,
this.spacing = 12,
this.tilePadding = 10,
this.runSpacing = 12,
@@ -51,12 +55,14 @@ class PaymentMethodSelectorRow extends StatelessWidget {
final availability = isAdded
? PaymentMethodTileAvailability.added
: PaymentMethodTileAvailability.available;
final detailsText = detailsBuilder?.call(entry);
tiles.add(
PaymentMethodTile(
type: type,
selection: selection,
availability: availability,
padding: tilePadding,
detailsText: detailsText,
onTap: () => onSelected(type, index),
),
);
@@ -68,6 +74,7 @@ class PaymentMethodSelectorRow extends StatelessWidget {
types: types,
disabledTypes: disabledTypes,
onAdd: onAdd,
onPressed: onAddPressed,
),
);
@@ -78,4 +85,4 @@ class PaymentMethodSelectorRow extends StatelessWidget {
children: tiles,
);
}
}
}

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/models/payment_method_tile/availability.dart';
import 'package:pweb/models/payment_method_tile/selection.dart';
import 'package:pweb/models/payment/method_tile/availability.dart';
import 'package:pweb/models/payment/method_tile/selection.dart';
import 'package:pweb/pages/payment_methods/icon.dart';
import 'package:pweb/utils/payment/label.dart';
@@ -15,6 +15,7 @@ class PaymentMethodTile extends StatelessWidget {
final PaymentMethodTileSelection selection;
final PaymentMethodTileAvailability availability;
final double padding;
final String? detailsText;
final VoidCallback? onTap;
const PaymentMethodTile({
@@ -22,6 +23,7 @@ class PaymentMethodTile extends StatelessWidget {
required this.selection,
required this.availability,
required this.padding,
this.detailsText,
required this.onTap,
});
@@ -50,6 +52,13 @@ class PaymentMethodTile extends StatelessWidget {
final backgroundColor = isSelected
? theme.colorScheme.primary.withValues(alpha: 0.08)
: theme.colorScheme.onSecondary;
final showDetails =
availability == PaymentMethodTileAvailability.added &&
detailsText != null &&
detailsText!.isNotEmpty;
final detailsColor = isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant;
return IntrinsicWidth(
child: Opacity(
@@ -68,9 +77,9 @@ class PaymentMethodTile extends StatelessWidget {
border: Border.all(color: borderColor),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
iconForPaymentType(type),
@@ -78,30 +87,44 @@ class PaymentMethodTile extends StatelessWidget {
color: isSelected ? theme.colorScheme.primary : theme.colorScheme.onSurface,
),
const SizedBox(width: 8),
Text(
label,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: isSelected ? theme.colorScheme.primary : theme.colorScheme.onSurface,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: isSelected ? theme.colorScheme.primary : theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
if (showDetails)
Text(
detailsText!,
style: theme.textTheme.labelSmall?.copyWith(
color: detailsColor,
fontWeight: FontWeight.w600,
),
)
else
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: badgeColor,
borderRadius: BorderRadius.circular(999),
),
child: Text(
badgeLabel,
style: theme.textTheme.labelSmall?.copyWith(
color: badgeTextColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: badgeColor,
borderRadius: BorderRadius.circular(999),
),
child: Text(
badgeLabel,
style: theme.textTheme.labelSmall?.copyWith(
color: badgeTextColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
),

View File

@@ -34,6 +34,8 @@ class RecipientAddressBookInfoRow extends StatelessWidget {
final style = textStyle ?? Theme.of(context).textTheme.bodySmall!;
return Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(iconForPaymentType(type), size: iconSize),
@@ -55,4 +57,4 @@ class RecipientAddressBookInfoRow extends StatelessWidget {
],
);
}
}
}

View File

@@ -59,6 +59,7 @@ class _RecipientAddressBookItemState extends State<RecipientAddressBookItem> {
child: Padding(
padding: widget.padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [

View File

@@ -29,12 +29,22 @@ class RecipientPaymentRow extends StatelessWidget {
return const Center(child: CircularProgressIndicator());
}
return Row(
spacing: spacing,
children: cacheProvider.methodsForRecipient(recipientId).map((m) => RecipientAddressBookInfoRow(
type: m.type,
value: getPaymentTypeDescription(context, m),
)).toList(),
return Align(
alignment: Alignment.centerLeft,
child: Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: spacing,
runSpacing: spacing,
children: cacheProvider
.methodsForRecipient(recipientId)
.map((m) => RecipientAddressBookInfoRow(
type: m.type,
value: getPaymentTypeDescription(context, m),
))
.toList(),
),
);
}
}

View File

@@ -6,7 +6,7 @@ import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/models/dashboard_payment_mode.dart';
import 'package:pweb/models/dashboard/dashboard_payment_mode.dart';
import 'package:pweb/pages/dashboard/buttons/balance/balance.dart';
import 'package:pweb/pages/dashboard/buttons/balance/controller.dart';
import 'package:pweb/pages/dashboard/buttons/buttons.dart';

View File

@@ -1,81 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentAmountWidget extends StatefulWidget {
const PaymentAmountWidget({super.key});
@override
State<PaymentAmountWidget> createState() => _PaymentAmountWidgetState();
}
class _PaymentAmountWidgetState extends State<PaymentAmountWidget> {
late final TextEditingController _controller;
bool _isSyncingText = false;
@override
void initState() {
super.initState();
final initialAmount = context.read<PaymentAmountProvider>().amount;
_controller = TextEditingController(text: amountToString(initialAmount));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double? _parseAmount(String value) {
final parsed = parseMoneyAmount(
value.replaceAll(',', '.'),
fallback: double.nan,
);
return parsed.isNaN ? null : parsed;
}
void _syncTextWithAmount(double amount) {
final parsedText = _parseAmount(_controller.text);
if (parsedText != null && parsedText == amount) return;
final nextText = amountToString(amount);
_isSyncingText = true;
_controller.value = TextEditingValue(
text: nextText,
selection: TextSelection.collapsed(offset: nextText.length),
);
_isSyncingText = false;
}
void _onChanged(String value) {
if (_isSyncingText) return;
final parsed = _parseAmount(value);
if (parsed != null) {
context.read<PaymentAmountProvider>().setAmount(parsed);
}
}
@override
Widget build(BuildContext context) {
final amount = context.select<PaymentAmountProvider, double>((provider) => provider.amount);
_syncTextWithAmount(amount);
return TextField(
controller: _controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.amount,
border: const OutlineInputBorder(),
),
onChanged: _onChanged,
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/currency.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/controllers/payments/amount_field.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentAmountField extends StatelessWidget {
const PaymentAmountField();
@override
Widget build(BuildContext context) {
final currency = context.select<WalletsController, Currency?>(
(c) => c.selectedWallet?.currency,
);
final symbol = currency == null ? null : currencyCodeToSymbol(currency);
final ui = context.watch<PaymentAmountFieldController>();
return TextField(
controller: ui.textController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.amount,
border: const OutlineInputBorder(),
prefixText: symbol == null ? null : '$symbol\u00A0',
),
onChanged: ui.handleChanged,
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pweb/controllers/payments/amount_field.dart';
import 'package:pweb/pages/dashboard/payouts/amount/feild.dart';
class PaymentAmountWidget extends StatelessWidget {
const PaymentAmountWidget({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<PaymentAmountProvider, PaymentAmountFieldController>(
create: (ctx) {
final initialAmount = ctx.read<PaymentAmountProvider>().amount;
return PaymentAmountFieldController(initialAmount: initialAmount);
},
update: (ctx, amountProvider, controller) {
controller!.update(amountProvider);
return controller;
},
child: const PaymentAmountField(),
);
}
}

View File

@@ -15,15 +15,30 @@ class FeePayerSwitch extends StatelessWidget {
@override
Widget build(BuildContext context) => Consumer<PaymentAmountProvider>(
builder: (context, provider, _) => Row(
spacing: spacing,
children: [
Text(AppLocalizations.of(context)!.recipientPaysFee, style: style),
Switch(
value: !provider.payerCoversFee,
onChanged: (val) => provider.setPayerCoversFee(!val),
builder: (context, provider, _) {
final recipientPaysFee = !provider.payerCoversFee;
final textStyle = style ?? Theme.of(context).textTheme.bodySmall;
void updateRecipientPaysFee(bool value) {
provider.setPayerCoversFee(!value);
}
return InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () => updateRecipientPaysFee(!recipientPaysFee),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: recipientPaysFee,
onChanged: (val) => updateRecipientPaysFee(val ?? false),
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
SizedBox(width: spacing),
Text(AppLocalizations.of(context)!.recipientPaysFee, style: textStyle),
],
),
],
),
);
},
);
}

View File

@@ -1,8 +1,13 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/dashboard/payouts/amount.dart';
import 'package:provider/provider.dart';
import 'package:pweb/controllers/payouts/quotation.dart';
import 'package:pweb/models/dashboard/quote_status_data.dart';
import 'package:pweb/pages/dashboard/payouts/amount/widget.dart';
import 'package:pweb/pages/dashboard/payouts/fee_payer.dart';
import 'package:pweb/pages/dashboard/payouts/quote_status/quote_status.dart';
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/card.dart';
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/refresh_section.dart';
import 'package:pweb/pages/dashboard/payouts/summary/widget.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -12,34 +17,134 @@ class PaymentFormWidget extends StatelessWidget {
const PaymentFormWidget({super.key});
static const double _smallSpacing = 5;
static const double _mediumSpacing = 10;
static const double _largeSpacing = 16;
static const double _mediumSpacing = 12;
static const double _largeSpacing = 20;
static const double _extraSpacing = 15;
static const double _columnSpacing = 24;
static const double _narrowWidth = 560;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
final controller = context.watch<QuotationController>();
final quoteStatus = QuoteStatusData.resolve(
controller: controller,
loc: loc,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(loc.details, style: theme.textTheme.titleMedium),
const SizedBox(height: _smallSpacing),
return LayoutBuilder(
builder: (context, constraints) {
final isNarrow = constraints.maxWidth < _narrowWidth;
const PaymentAmountWidget(),
final detailsHeader = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(loc.details, style: theme.textTheme.titleMedium),
const SizedBox(height: _smallSpacing),
],
);
const SizedBox(height: _mediumSpacing),
final quoteCard = QuoteStatusCard(
statusType: quoteStatus.statusType,
isLoading: quoteStatus.isLoading,
statusText: quoteStatus.statusText,
helperText: quoteStatus.helperText,
canRefresh: quoteStatus.canRefresh,
showPrimaryRefresh: quoteStatus.showPrimaryRefresh,
onRefresh: controller.refreshQuotation,
);
FeePayerSwitch(spacing: _mediumSpacing, style: theme.textTheme.titleMedium),
final autoRefreshSection = QuoteAutoRefreshSection(
autoRefreshMode: quoteStatus.autoRefreshMode,
canRefresh: quoteStatus.canRefresh,
onModeChanged: controller.setAutoRefreshMode,
);
const SizedBox(height: _largeSpacing),
final leftColumn = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const PaymentAmountWidget(),
const SizedBox(height: _smallSpacing),
FeePayerSwitch(
spacing: _smallSpacing,
style: theme.textTheme.bodySmall,
),
const SizedBox(height: _mediumSpacing),
const PaymentSummary(spacing: _extraSpacing),
],
);
const PaymentSummary(spacing: _extraSpacing),
const SizedBox(height: _mediumSpacing),
const QuoteStatus(spacing: _smallSpacing),
],
final rightColumn = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
quoteCard,
const SizedBox(height: _smallSpacing),
autoRefreshSection,
],
);
if (isNarrow) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
detailsHeader,
leftColumn,
const SizedBox(height: _largeSpacing),
rightColumn,
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
detailsHeader,
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const PaymentAmountWidget(),
const SizedBox(height: _smallSpacing),
FeePayerSwitch(
spacing: _smallSpacing,
style: theme.textTheme.bodySmall,
),
],
),
),
const SizedBox(width: _columnSpacing),
Expanded(flex: 2, child: quoteCard),
],
),
const SizedBox(height: _mediumSpacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Expanded(
flex: 3,
child: PaymentSummary(spacing: _extraSpacing),
),
const SizedBox(width: _columnSpacing),
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
autoRefreshSection,
],
),
),
],
),
],
);
},
);
}
}

View File

@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payout_verification.dart';
import 'package:pshared/provider/payment/multiple/quotation.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/payout_verification.dart';
import 'package:pweb/utils/payment/payout_verification_flow.dart';
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
@@ -13,9 +15,13 @@ Future<void> handleMultiplePayoutSend(
MultiplePayoutsController controller,
) async {
final verificationController = context.read<PayoutVerificationController>();
final quotationProvider = context.read<MultiQuotationProvider>();
final verificationContextKey = quotationProvider.quotation?.quoteRef ??
quotationProvider.quotation?.idempotencyKey;
final verified = await runPayoutVerification(
context: context,
controller: verificationController,
contextKey: verificationContextKey,
);
if (!verified) return;

View File

@@ -3,7 +3,7 @@ import 'package:pshared/models/money.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
String moneyLabel(Money? money) {
@@ -32,8 +32,5 @@ String sentAmountLabel(MultiplePayoutsController controller) {
}
String feeLabel(MultiplePayoutsController controller) {
final feeLabelText = moneyLabel(controller.aggregateFeeAmount);
final percent = controller.aggregateFeePercent;
if (percent == null) return feeLabelText;
return '$feeLabelText (${percent.toStringAsFixed(2)}%)';
return moneyLabel(controller.aggregateFeeAmount);
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/models/summary_values.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/models/dashboard/summary_values.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart';
import 'package:pweb/pages/dashboard/payouts/summary/widget.dart';
@@ -21,7 +21,6 @@ class SourceQuoteSummary extends StatelessWidget {
return PaymentSummary(
spacing: spacing,
values: PaymentSummaryValues(
sentAmount: sentAmountLabel(controller),
fee: feeLabel(controller),
recipientReceives: moneyLabel(controller.aggregateSettlementAmount),
total: moneyLabel(controller.aggregateDebitAmount),

View File

@@ -1,19 +1,20 @@
import 'package:flutter/material.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:provider/provider.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payout_verification.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/provider/payment/multiple/quotation.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/payout_verification.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/actions.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/header.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/widgets/quote_status.dart';
import 'package:pweb/pages/payment_methods/payment_page/send_button.dart';
import 'package:pweb/pages/payout_page/send/widgets/send_button.dart';
import 'package:pweb/widgets/payment/source_wallet_selector.dart';
import 'package:pweb/widgets/cooldown_hint.dart';
import 'package:pweb/models/control_state.dart';
import 'package:provider/provider.dart';
import 'package:pweb/models/state/control_state.dart';
class SourceQuotePanel extends StatelessWidget {
@@ -31,7 +32,12 @@ class SourceQuotePanel extends StatelessWidget {
final theme = Theme.of(context);
final verificationController =
context.watch<PayoutVerificationController>();
final isCooldownActive = verificationController.isCooldownActive;
final quotationProvider = context.watch<MultiQuotationProvider>();
final verificationContextKey = quotationProvider.quotation?.quoteRef ??
quotationProvider.quotation?.idempotencyKey;
final isCooldownActive = verificationController.isCooldownActiveFor(
verificationContextKey,
);
final canSend = controller.canSend && !isCooldownActive;
return Container(
width: double.infinity,
@@ -72,7 +78,9 @@ class SourceQuotePanel extends StatelessWidget {
if (isCooldownActive) ...[
const SizedBox(height: 8),
CooldownHint(
seconds: verificationController.cooldownRemainingSeconds,
seconds: verificationController.cooldownRemainingSecondsFor(
verificationContextKey,
),
),
],
],

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:dotted_border/dotted_border.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart';

View File

@@ -6,7 +6,7 @@ import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/provider/payment/payments.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/history/header.dart';
import 'package:pweb/controllers/recent_payments.dart';
import 'package:pweb/controllers/payments/recent_payments.dart';
import 'package:pweb/pages/report/cards/column.dart';
import 'package:pweb/utils/report/payment_mapper.dart';
import 'package:pweb/app/router/payout_routes.dart';

View File

@@ -1,4 +1,4 @@
import 'package:pweb/models/multiple_payouts/csv_row.dart';
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
const String sampleFileName = 'sample.csv';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pweb/models/multiple_payouts/csv_row.dart';
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart';

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart';

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/quote/status_type.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/card.dart';
import 'package:pweb/utils/quote_duration_format.dart';

View File

@@ -1,70 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/quote/status_type.dart';
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/body.dart';
import 'package:pweb/providers/quotation/quotation.dart';
import 'package:pweb/utils/quote_duration_format.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class QuoteStatus extends StatelessWidget {
final double spacing;
const QuoteStatus({super.key, required this.spacing});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final controller = context.watch<QuotationController>();
final timeLeft = controller.timeLeft;
final isLoading = controller.isLoading;
final statusType = controller.quoteStatus;
final autoRefreshMode = controller.autoRefreshMode;
String statusText;
String? helperText;
switch (statusType) {
case QuoteStatusType.loading:
statusText = loc.quoteUpdating;
break;
case QuoteStatusType.error:
statusText = loc.quoteErrorGeneric;
break;
case QuoteStatusType.missing:
statusText = loc.quoteUnavailable;
break;
case QuoteStatusType.expired:
statusText = loc.quoteExpired;
helperText = loc.quoteRefreshRequired;
break;
case QuoteStatusType.active:
statusText = timeLeft == null
? loc.quoteActive
: loc.quoteExpiresIn(formatQuoteDuration(timeLeft));
break;
}
final canRefresh = controller.canRefresh && !isLoading;
final showPrimaryRefresh = canRefresh &&
(statusType == QuoteStatusType.expired ||
statusType == QuoteStatusType.error ||
statusType == QuoteStatusType.missing);
return QuoteStatusBody(
spacing: spacing,
statusType: statusType,
statusText: statusText,
helperText: helperText,
isLoading: isLoading,
canRefresh: canRefresh,
showPrimaryRefresh: showPrimaryRefresh,
autoRefreshMode: autoRefreshMode,
onAutoRefreshModeChanged: controller.setAutoRefreshMode,
onRefresh: controller.refreshQuotation,
);
}
}

View File

@@ -48,34 +48,14 @@ class QuoteAutoRefreshSection extends StatelessWidget {
),
),
const SizedBox(width: _autoRefreshSpacing),
ToggleButtons(
isSelected: [
autoRefreshMode == AutoRefreshMode.off,
autoRefreshMode == AutoRefreshMode.on,
],
onPressed: canRefresh
? (index) {
final nextMode =
index == 1 ? AutoRefreshMode.on : AutoRefreshMode.off;
if (nextMode == autoRefreshMode) return;
onModeChanged(nextMode);
}
Switch.adaptive(
activeTrackColor: theme.colorScheme.primary,
value: autoRefreshMode == AutoRefreshMode.on,
onChanged: canRefresh
? (value) => onModeChanged(
value ? AutoRefreshMode.on : AutoRefreshMode.off,
)
: null,
borderRadius: BorderRadius.circular(999),
constraints: const BoxConstraints(minHeight: 32, minWidth: 56),
selectedColor: theme.colorScheme.onPrimary,
fillColor: theme.colorScheme.primary,
color: theme.colorScheme.onSurface,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(loc.toggleOff),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(loc.toggleOn),
),
],
),
],
);

View File

@@ -26,6 +26,7 @@ class RecipientAvatar extends StatelessWidget {
final textColor = Theme.of(context).colorScheme.onPrimary;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: avatarRadius,

View File

@@ -7,11 +7,13 @@ import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart';
class ShortListAddressBookPayout extends StatelessWidget {
final List<Recipient> recipients;
final ValueChanged<Recipient> onSelected;
final Widget? trailing;
const ShortListAddressBookPayout({
super.key,
required this.recipients,
required this.onSelected,
this.trailing,
});
static const double _avatarRadius = 20;
@@ -21,10 +23,13 @@ class ShortListAddressBookPayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
final trailingWidget = trailing;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: recipients.map((recipient) {
children:
recipients.map((recipient) {
return Padding(
padding: _padding,
child: InkWell(
@@ -44,8 +49,13 @@ class ShortListAddressBookPayout extends StatelessWidget {
),
),
);
}).toList(),
}).toList()
..addAll(
trailingWidget == null
? const []
: [Padding(padding: _padding, child: trailingWidget)],
),
),
);
}
}
}

View File

@@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/asset.dart';
import 'package:pshared/models/currency.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentSentAmountRow extends StatelessWidget {
final Currency currency;
const PaymentSentAmountRow({super.key, required this.currency});
@override
Widget build(BuildContext context) => Consumer<PaymentAmountProvider>(
builder: (context, provider, _) => PaymentSummaryRow(
labelFactory: AppLocalizations.of(context)!.sentAmount,
asset: Asset(currency: currency, amount: provider.amount),
style: Theme.of(context).textTheme.titleMedium,
),
);
}

View File

@@ -1,15 +1,9 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/models/summary_values.dart';
import 'package:pweb/models/dashboard/summary_values.dart';
import 'package:pweb/pages/dashboard/payouts/summary/fee.dart';
import 'package:pweb/pages/dashboard/payouts/summary/recipient_receives.dart';
import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
import 'package:pweb/pages/dashboard/payouts/summary/sent_amount.dart';
import 'package:pweb/pages/dashboard/payouts/summary/total.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -36,12 +30,6 @@ class PaymentSummary extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
PaymentSummaryRow(
labelFactory: loc.sentAmount,
asset: null,
value: resolvedValues.sentAmount,
style: theme.textTheme.titleMedium,
),
PaymentSummaryRow(
labelFactory: loc.fee,
asset: null,
@@ -73,12 +61,6 @@ class PaymentSummary extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
PaymentSentAmountRow(
currency: currencyStringToCode(
context.read<WalletsController>().selectedWallet?.tokenSymbol ??
'USDT',
),
),
const PaymentFeeRow(),
const PaymentRecipientReceivesRow(),
SizedBox(height: spacing),

View File

@@ -1,164 +0,0 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/resources.dart';
import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/invitations.dart';
import 'package:pshared/provider/permissions.dart';
import 'package:pweb/pages/invitations/widgets/header.dart';
import 'package:pweb/pages/invitations/widgets/form/form.dart';
import 'package:pweb/pages/invitations/widgets/list/list.dart';
import 'package:pweb/pages/loader.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/widgets/roles/create_role_dialog.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class InvitationsPage extends StatefulWidget {
const InvitationsPage({super.key});
@override
State<InvitationsPage> createState() => _InvitationsPageState();
}
class _InvitationsPageState extends State<InvitationsPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _firstNameController = TextEditingController();
final TextEditingController _lastNameController = TextEditingController();
final TextEditingController _messageController = TextEditingController();
String? _selectedRoleRef;
int _expiryDays = 7;
Future<void> _createRole() async {
final loc = AppLocalizations.of(context)!;
final draft = await showCreateRoleDialog(context);
if (draft == null) return;
final permissions = context.read<PermissionsProvider>();
final createdRole = await executeActionWithNotification(
context: context,
action: () => permissions.createRoleDescription(
name: draft.name,
description: draft.description.isEmpty ? null : draft.description,
),
successMessage: loc.invitationRoleCreated,
errorMessage: loc.invitationRoleCreateFailed,
);
if (createdRole != null && mounted) {
setState(() => _selectedRoleRef = createdRole.id);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_bootstrapRoleSelection();
}
void _bootstrapRoleSelection() {
final roles = context.read<PermissionsProvider>().roleDescriptions;
if (roles.isEmpty) return;
final firstRoleRef = roles.first.storable.id;
final isSelectedAvailable = _selectedRoleRef != null
&& roles.any((role) => role.storable.id == _selectedRoleRef);
if (isSelectedAvailable) return;
if (!mounted) return;
setState(() => _selectedRoleRef = firstRoleRef);
}
@override
void dispose() {
_emailController.dispose();
_firstNameController.dispose();
_lastNameController.dispose();
_messageController.dispose();
super.dispose();
}
Future<void> _sendInvitation() async {
final form = _formKey.currentState;
if (form == null || !form.validate()) return;
final account = context.read<AccountProvider>().account;
if (account == null) return;
final permissions = context.read<PermissionsProvider>();
final roleRef = _selectedRoleRef ?? permissions.roleDescriptions.firstOrNull?.storable.id;
if (roleRef == null) return;
final invitations = context.read<InvitationsProvider>();
final loc = AppLocalizations.of(context)!;
await executeActionWithNotification(
context: context,
action: () => invitations.sendInvitation(
email: _emailController.text.trim(),
name: _firstNameController.text.trim(),
lastName: _lastNameController.text.trim(),
comment: _messageController.text.trim(),
roleRef: roleRef,
inviterRef: account.id,
expiresAt: DateTime.now().toUtc().add(Duration(days: _expiryDays)),
),
successMessage: loc.invitationCreatedSuccess,
errorMessage: loc.errorCreatingInvitation,
);
_emailController.clear();
_firstNameController.clear();
_lastNameController.clear();
_messageController.clear();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final permissions = context.watch<PermissionsProvider>();
final canCreateRoles = permissions.canCreate(ResourceType.roles);
if (!permissions.canRead(ResourceType.invitations)) {
return PageViewLoader(
child: Center(child: Text(loc.errorAccessDenied)),
);
}
return PageViewLoader(
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InvitationsHeader(loc: loc),
const SizedBox(height: 16),
InvitationsForm(
formKey: _formKey,
emailController: _emailController,
firstNameController: _firstNameController,
lastNameController: _lastNameController,
messageController: _messageController,
canCreateRoles: canCreateRoles,
onCreateRole: _createRole,
expiryDays: _expiryDays,
onExpiryChanged: (value) => setState(() => _expiryDays = value),
selectedRoleRef: _selectedRoleRef,
onRoleChanged: (role) => setState(() => _selectedRoleRef = role),
canCreate: permissions.canCreate(ResourceType.invitations),
onSubmit: _sendInvitation,
),
const SizedBox(height: 24),
const InvitationsList(),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/controllers/invitations/page.dart';
import 'package:pweb/pages/invitations/page/providers.dart';
import 'package:pweb/pages/invitations/page/view.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class InvitationsPage extends StatefulWidget {
const InvitationsPage({super.key});
@override
State<InvitationsPage> createState() => _InvitationsPageState();
}
class _InvitationsPageState extends State<InvitationsPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _firstNameController = TextEditingController();
final TextEditingController _lastNameController = TextEditingController();
final TextEditingController _messageController = TextEditingController();
Future<void> _sendInvitation(BuildContext context) async {
final form = _formKey.currentState;
if (form == null || !form.validate()) return;
final loc = AppLocalizations.of(context)!;
await executeActionWithNotification(
context: context,
action: () => context.read<InvitationsPageController>().sendInvitation(
email: _emailController.text,
name: _firstNameController.text,
lastName: _lastNameController.text,
comment: _messageController.text,
),
successMessage: loc.invitationCreatedSuccess,
errorMessage: loc.errorCreatingInvitation,
);
_emailController.clear();
_firstNameController.clear();
_lastNameController.clear();
_messageController.clear();
}
@override
void dispose() {
_emailController.dispose();
_firstNameController.dispose();
_lastNameController.dispose();
_messageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return InvitationsPageProviders(
child: Builder(
builder: (context) => InvitationsPageView(
formKey: _formKey,
emailController: _emailController,
firstNameController: _firstNameController,
lastNameController: _lastNameController,
messageController: _messageController,
onSubmit: () => _sendInvitation(context),
),
),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/invitations.dart';
import 'package:pshared/provider/permissions.dart';
import 'package:pweb/controllers/invitations/page.dart';
class InvitationsPageProviders extends StatelessWidget {
final Widget child;
const InvitationsPageProviders({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider3<
PermissionsProvider,
InvitationsProvider,
AccountProvider,
InvitationsPageController
>(
create: (_) => InvitationsPageController(),
update: (_, permissions, invitations, account, controller) => controller!
..update(
permissions: permissions,
invitations: invitations,
account: account,
),
child: child,
);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/resources.dart';
import 'package:pshared/provider/permissions.dart';
import 'package:pweb/controllers/invitations/page.dart';
import 'package:pweb/pages/invitations/widgets/header.dart';
import 'package:pweb/pages/invitations/widgets/form/form.dart';
import 'package:pweb/pages/invitations/widgets/list/list.dart';
import 'package:pweb/pages/loader.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class InvitationsPageView extends StatelessWidget {
final GlobalKey<FormState> formKey;
final TextEditingController emailController;
final TextEditingController firstNameController;
final TextEditingController lastNameController;
final TextEditingController messageController;
final VoidCallback onSubmit;
const InvitationsPageView({
super.key,
required this.formKey,
required this.emailController,
required this.firstNameController,
required this.lastNameController,
required this.messageController,
required this.onSubmit,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final permissions = context.watch<PermissionsProvider>();
final canCreateRoles = permissions.canCreate(ResourceType.roles);
final ui = context.watch<InvitationsPageController>();
if (!permissions.canRead(ResourceType.invitations)) {
return PageViewLoader(
child: Center(child: Text(loc.errorAccessDenied)),
);
}
return PageViewLoader(
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InvitationsHeader(loc: loc),
const SizedBox(height: 16),
InvitationsForm(
formKey: formKey,
emailController: emailController,
firstNameController: firstNameController,
lastNameController: lastNameController,
messageController: messageController,
canCreateRoles: canCreateRoles,
expiryDays: ui.expiryDays,
onExpiryChanged: ui.setExpiryDays,
selectedRoleRef: ui.selectedRoleRef,
onRoleChanged: ui.setSelectedRoleRef,
canCreate: permissions.canCreate(ResourceType.invitations),
onSubmit: onSubmit,
),
const SizedBox(height: 24),
const InvitationsList(),
],
),
),
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pweb/models/invitation_filter.dart';
import 'package:pweb/models/invitation/invitation_filter.dart';
import 'package:pweb/pages/invitations/widgets/filter/invitation_filter.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -1,7 +1,7 @@
import 'package:pshared/models/invitation/invitation.dart';
import 'package:pshared/models/invitation/status.dart';
import 'package:pweb/models/invitation_filter.dart';
import 'package:pweb/models/invitation/invitation_filter.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -10,7 +10,6 @@ class InvitationsForm extends StatelessWidget {
final TextEditingController lastNameController;
final TextEditingController messageController;
final bool canCreateRoles;
final VoidCallback onCreateRole;
final int expiryDays;
final ValueChanged<int> onExpiryChanged;
final String? selectedRoleRef;
@@ -26,7 +25,6 @@ class InvitationsForm extends StatelessWidget {
required this.lastNameController,
required this.messageController,
required this.canCreateRoles,
required this.onCreateRole,
required this.expiryDays,
required this.onExpiryChanged,
required this.selectedRoleRef,
@@ -43,7 +41,6 @@ class InvitationsForm extends StatelessWidget {
lastNameController: lastNameController,
messageController: messageController,
canCreateRoles: canCreateRoles,
onCreateRole: onCreateRole,
expiryDays: expiryDays,
onExpiryChanged: onExpiryChanged,
selectedRoleRef: selectedRoleRef,

View File

@@ -17,7 +17,6 @@ class InvitationFormView extends StatelessWidget {
final TextEditingController lastNameController;
final TextEditingController messageController;
final bool canCreateRoles;
final VoidCallback onCreateRole;
final int expiryDays;
final ValueChanged<int> onExpiryChanged;
final String? selectedRoleRef;
@@ -33,7 +32,6 @@ class InvitationFormView extends StatelessWidget {
required this.lastNameController,
required this.messageController,
required this.canCreateRoles,
required this.onCreateRole,
required this.expiryDays,
required this.onExpiryChanged,
required this.selectedRoleRef,

View File

@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
import 'package:pshared/provider/invitations.dart';
import 'package:pweb/models/invitation_filter.dart';
import 'package:pweb/models/invitation/invitation_filter.dart';
import 'package:pweb/pages/invitations/widgets/filter/chips.dart';
import 'package:pweb/pages/invitations/widgets/list/body.dart';
import 'package:pweb/pages/invitations/widgets/list/view_model.dart';

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/invitation/invitation.dart';
import 'package:pweb/models/invitation_filter.dart';
import 'package:pweb/models/invitation/invitation_filter.dart';
import 'package:pweb/pages/invitations/widgets/filter/invitation_filter.dart';

View File

@@ -6,7 +6,9 @@ import 'package:pshared/models/auth/state.dart';
import 'package:pshared/provider/account.dart';
import 'package:pweb/app/router/pages.dart';
import 'package:pweb/controllers/auth/account_loader.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/models/account/account_loader.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -20,29 +22,29 @@ class AccountLoader extends StatefulWidget {
}
class _AccountLoaderState extends State<AccountLoader> {
AuthState? _handledState;
late final AccountLoaderController _controller;
@override
void initState() {
super.initState();
_controller = AccountLoaderController()..addListener(_handleControllerAction);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
Provider.of<AccountProvider>(context, listen: false).restoreIfPossible();
});
}
void _handleSideEffects(AccountProvider provider) {
if (_handledState == provider.authState) return;
_handledState = provider.authState;
void _handleControllerAction() {
final action = _controller.consumeAction();
if (action == null) return;
void goToLogin() {
if (!mounted) return;
navigateAndReplace(context, Pages.login);
}
switch (provider.authState) {
case AuthState.error:
final error = provider.error ?? Exception('Authorization failed');
switch (action) {
case AccountLoaderAction.showErrorAndGoToLogin:
final error = _controller.error ?? Exception('Authorization failed');
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
postNotifyUserOfErrorX(
@@ -53,18 +55,23 @@ class _AccountLoaderState extends State<AccountLoader> {
goToLogin();
});
break;
case AuthState.empty:
case AccountLoaderAction.goToLogin:
WidgetsBinding.instance.addPostFrameCallback((_) => goToLogin());
break;
default:
break;
}
}
@override
void dispose() {
_controller.removeListener(_handleControllerAction);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer<AccountProvider>(builder: (context, provider, _) {
_handleSideEffects(provider);
_controller.update(provider);
if (provider.authState == AuthState.ready && provider.account != null) {
return widget.child;
}

View File

@@ -32,9 +32,6 @@ class OrganizationLoader extends StatelessWidget {
);
}
if ((provider.error == null) && (!provider.isOrganizationSet)) {
WidgetsBinding.instance.addPostFrameCallback((_) {
provider.load();
});
return const Center(child: CircularProgressIndicator());
}
return child;

View File

@@ -12,16 +12,6 @@ class PermissionsLoader extends StatelessWidget {
final Widget child;
const PermissionsLoader({super.key, required this.child});
void _triggerLoadIfNeeded(PermissionsProvider provider) {
if (!provider.isLoading && !provider.isReady) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!provider.isLoading && !provider.isReady) {
provider.load();
}
});
}
}
@override
Widget build(BuildContext context) {
return Consumer2<PermissionsProvider, AccountProvider>(
@@ -42,7 +32,6 @@ class PermissionsLoader extends StatelessWidget {
),
);
}
_triggerLoadIfNeeded(provider);
if (provider.isLoading || !provider.isReady) {
return const Center(child: CircularProgressIndicator());
}

View File

@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -15,9 +13,8 @@ import 'package:pweb/widgets/password/hint/short.dart';
import 'package:pweb/widgets/password/password.dart';
import 'package:pweb/widgets/username.dart';
import 'package:pweb/widgets/vspacer.dart';
import 'package:pweb/controllers/email.dart';
import 'package:pweb/controllers/auth/email.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -57,7 +54,6 @@ class _LoginFormState extends State<LoginForm> {
password: _passwordController.text,
locale: context.read<LocaleProvider>().locale.languageCode,
);
unawaited(PosthogService.login(pending: outcome.isPending));
if (outcome.isPending) {
// TODO: fix context usage
navigateAndReplace(context, Pages.sfactor);

View File

@@ -4,8 +4,8 @@ import 'package:provider/provider.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pweb/pages/payment_methods/title.dart';
import 'package:pweb/pages/payout_page/methods/controller.dart';
import 'package:pweb/pages/payment_methods/manage/method_tile.dart';
import 'package:pweb/controllers/payments/payment_config.dart';
class PaymentConfigList extends StatelessWidget {

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/payout_page/methods/advanced.dart';
import 'package:pweb/pages/payout_page/methods/controller.dart';
import 'package:pweb/pages/payout_page/methods/header.dart';
import 'package:pweb/pages/payout_page/methods/list.dart';
import 'package:pweb/pages/payment_methods/manage/advanced.dart';
import 'package:pweb/controllers/payments/payment_config.dart';
import 'package:pweb/pages/payment_methods/manage/header.dart';
import 'package:pweb/pages/payment_methods/manage/list.dart';
class MethodsWidget extends StatefulWidget {

View File

@@ -1,162 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/payment/flow.dart';
import 'package:pshared/provider/payment/provider.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payment_methods/payment_page/body.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/utils/recipient/filtering.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
import 'package:pweb/controllers/payment_page.dart';
import 'package:pweb/controllers/payout_verification.dart';
import 'package:pweb/utils/payment/payout_verification_flow.dart';
import 'package:pweb/models/control_state.dart';
class PaymentPage extends StatefulWidget {
final ValueChanged<Recipient?>? onBack;
final PaymentType? initialPaymentType;
final PayoutDestination fallbackDestination;
const PaymentPage({
super.key,
this.onBack,
this.initialPaymentType,
this.fallbackDestination = PayoutDestination.dashboard,
});
@override
State<PaymentPage> createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> {
late final TextEditingController _searchController;
late final FocusNode _searchFocusNode;
Recipient? _previousRecipient;
String _query = '';
@override
void initState() {
super.initState();
_searchController = TextEditingController();
_searchFocusNode = FocusNode();
WidgetsBinding.instance.addPostFrameCallback((_) => _initializePaymentPage());
}
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
void _initializePaymentPage() {
final flowProvider = context.read<PaymentFlowProvider>();
flowProvider.setPreferredType(widget.initialPaymentType);
}
void _handleSearchChanged(String query) {
setState(() {
_query = query;
});
}
void _handleRecipientSelected(Recipient recipient) {
final recipientProvider = context.read<RecipientsProvider>();
setState(() {
_previousRecipient = recipientProvider.currentObject;
});
recipientProvider.setCurrentObject(recipient.id);
_clearSearchField();
}
void _handleRecipientCleared() {
final recipientProvider = context.read<RecipientsProvider>();
setState(() {
_previousRecipient = recipientProvider.currentObject;
});
recipientProvider.setCurrentObject(null);
_clearSearchField();
}
void _clearSearchField() {
_searchController.clear();
_searchFocusNode.unfocus();
setState(() {
_query = '';
});
}
Future<void> _handleSendPayment() async {
final flowProvider = context.read<PaymentFlowProvider>();
final paymentProvider = context.read<PaymentProvider>();
final controller = context.read<PaymentPageController>();
final verificationController = context.read<PayoutVerificationController>();
if (paymentProvider.isLoading) return;
final verified = await runPayoutVerification(
context: context,
controller: verificationController,
);
if (!verified || !mounted) return;
final isSuccess = await controller.sendPayment();
if (!mounted) return;
await showPaymentStatusDialog(context, isSuccess: isSuccess);
if (!mounted) return;
if (isSuccess) {
PosthogService.paymentInitiated(method: flowProvider.selectedType);
controller.resetAfterSuccess();
context.goToPayout(widget.fallbackDestination);
}
}
@override
Widget build(BuildContext context) {
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipientProvider = context.watch<RecipientsProvider>();
final verificationController =
context.watch<PayoutVerificationController>();
final recipient = recipientProvider.currentObject;
final filteredRecipients = filterRecipients(
recipients: recipientProvider.recipients,
query: _query,
);
final sendState = verificationController.isCooldownActive
? ControlState.disabled
: ControlState.enabled;
return PaymentPageBody(
onBack: widget.onBack,
fallbackDestination: widget.fallbackDestination,
recipient: recipient,
previousRecipient: _previousRecipient,
recipientProvider: recipientProvider,
searchQuery: _query,
filteredRecipients: filteredRecipients,
methodsProvider: methodsProvider,
sendState: sendState,
cooldownRemainingSeconds:
verificationController.cooldownRemainingSeconds,
onWalletSelected: context.read<WalletsController>().selectWallet,
searchController: _searchController,
searchFocusNode: _searchFocusNode,
onSearchChanged: _handleSearchChanged,
onRecipientSelected: _handleRecipientSelected,
onRecipientCleared: _handleRecipientCleared,
onSend: _handleSendPayment,
);
}
}

View File

@@ -1,134 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payment_methods/payment_page/back_button.dart';
import 'package:pweb/pages/payment_methods/payment_page/header.dart';
import 'package:pweb/pages/payment_methods/payment_page/method_selector.dart';
import 'package:pweb/pages/payment_methods/payment_page/send_button.dart';
import 'package:pweb/pages/dashboard/payouts/form.dart';
import 'package:pweb/pages/payment_methods/widgets/payment_info_section.dart';
import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/widgets/refresh_balance/wallet.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageContent extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final Recipient? previousRecipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onSend;
const PaymentPageContent({
super.key,
required this.onBack,
required this.recipient,
required this.previousRecipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.onWalletSelected,
required this.fallbackDestination,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onSend,
});
@override
Widget build(BuildContext context) {
final dimensions = AppDimensions();
final loc = AppLocalizations.of(context)!;
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth),
child: Material(
elevation: dimensions.elevationSmall,
borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium),
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: EdgeInsets.all(dimensions.paddingLarge),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PaymentBackButton(
onBack: onBack,
recipient: recipient,
fallbackDestination: fallbackDestination,
),
SizedBox(height: dimensions.paddingSmall),
PaymentHeader(),
SizedBox(height: dimensions.paddingXXLarge),
Row(
children: [
Expanded(child: SectionTitle(loc.sourceOfFunds)),
Consumer<WalletsController>(
builder: (context, provider, _) {
final selectedWalletId = provider.selectedWallet?.id;
if (selectedWalletId == null) {
return const SizedBox.shrink();
}
return WalletBalanceRefreshButton(walletRef: selectedWalletId);
},
),
],
),
SizedBox(height: dimensions.paddingSmall),
PaymentMethodSelector(
onMethodChanged: onWalletSelected,
),
SizedBox(height: dimensions.paddingXLarge),
RecipientSection(
recipient: recipient,
previousRecipient: previousRecipient,
dimensions: dimensions,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
),
SizedBox(height: dimensions.paddingXLarge),
PaymentInfoSection(dimensions: dimensions),
SizedBox(height: dimensions.paddingLarge),
const PaymentFormWidget(),
SizedBox(height: dimensions.paddingXXXLarge),
SendButton(onPressed: onSend),
SizedBox(height: dimensions.paddingLarge),
],
),
),
),
),
),
);
}
}

View File

@@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentHeader extends StatelessWidget {
const PaymentHeader({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dimensions = AppDimensions();
return 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
),
),
],
);
}
}

View File

@@ -1,136 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/dashboard/payouts/form.dart';
import 'package:pweb/pages/payment_methods/payment_page/back_button.dart';
import 'package:pweb/pages/payment_methods/payment_page/header.dart';
import 'package:pweb/pages/payment_methods/payment_page/method_selector.dart';
import 'package:pweb/pages/payment_methods/payment_page/send_button.dart';
import 'package:pweb/pages/payment_methods/widgets/payment_info_section.dart';
import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/widgets/cooldown_hint.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/models/control_state.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageContent extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final Recipient? previousRecipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination;
final ControlState sendState;
final int cooldownRemainingSeconds;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onSend;
const PaymentPageContent({
super.key,
required this.onBack,
required this.recipient,
required this.previousRecipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.onWalletSelected,
required this.fallbackDestination,
required this.sendState,
required this.cooldownRemainingSeconds,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onSend,
});
@override
Widget build(BuildContext context) {
final dimensions = AppDimensions();
final loc = AppLocalizations.of(context)!;
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth),
child: Material(
elevation: dimensions.elevationSmall,
borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium),
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: EdgeInsets.all(dimensions.paddingLarge),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PaymentBackButton(
onBack: onBack,
recipient: recipient,
fallbackDestination: fallbackDestination,
),
SizedBox(height: dimensions.paddingSmall),
PaymentHeader(),
SizedBox(height: dimensions.paddingXXLarge),
SectionTitle(loc.sourceOfFunds),
SizedBox(height: dimensions.paddingSmall),
PaymentMethodSelector(
onMethodChanged: onWalletSelected,
),
SizedBox(height: dimensions.paddingXLarge),
RecipientSection(
recipient: recipient,
previousRecipient: previousRecipient,
dimensions: dimensions,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
),
SizedBox(height: dimensions.paddingXLarge),
PaymentInfoSection(dimensions: dimensions),
SizedBox(height: dimensions.paddingLarge),
const PaymentFormWidget(),
SizedBox(height: dimensions.paddingXXXLarge),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SendButton(
onPressed: onSend,
state: sendState,
),
if (sendState == ControlState.disabled &&
cooldownRemainingSeconds > 0) ...[
SizedBox(height: dimensions.paddingSmall),
CooldownHint(seconds: cooldownRemainingSeconds),
],
],
),
SizedBox(height: dimensions.paddingLarge),
],
),
),
),
),
),
);
}
}

View File

@@ -1,98 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/provider/payment/flow.dart';
import 'package:pweb/pages/payment_methods/form.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/utils/payment/availability.dart';
import 'package:pweb/utils/payment/selector_type.dart';
import 'package:pweb/utils/payment/label.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
//TODO Whole page sucks. Will redesign.
class PaymentInfoSection extends StatelessWidget {
final AppDimensions dimensions;
const PaymentInfoSection({
super.key,
required this.dimensions,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final flowProvider = context.watch<PaymentFlowProvider>();
final hasRecipient = flowProvider.hasRecipient;
final MethodMap resolvedAvailableTypes = filterVisiblePaymentTypes(flowProvider.availableTypes);
final disabledTypesForSelection = hasRecipient
? disabledPaymentTypes.difference(resolvedAvailableTypes.keys.toSet())
: disabledPaymentTypes;
final methodsForSelectedType = flowProvider.methodsForSelectedType;
final selectedMethod = flowProvider.selectedMethod ??
(methodsForSelectedType.isNotEmpty ? methodsForSelectedType.first : null);
if (hasRecipient && resolvedAvailableTypes.isEmpty) {
return Text(loc.recipientNoPaymentDetails);
}
final selectedType = flowProvider.selectedType;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(loc.paymentInfo),
SizedBox(height: dimensions.paddingSmall),
PaymentTypeSelector(
availableTypes: resolvedAvailableTypes,
selectedType: selectedType,
disabledTypes: disabledTypesForSelection,
onSelected: (type) => flowProvider.selectType(
type,
resetManualData: !hasRecipient,
),
),
SizedBox(height: dimensions.paddingMedium),
if (hasRecipient && methodsForSelectedType.length > 1)
DropdownButtonFormField<PaymentMethod>(
initialValue: selectedMethod,
dropdownColor: Theme.of(context).colorScheme.onSecondary,
decoration: InputDecoration(
labelText: loc.paymentMethodDetails,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
items: methodsForSelectedType.map((method) {
final description = getPaymentTypeDescription(context, method);
final label = method.name.isNotEmpty ? '${method.name} - $description' : description;
return DropdownMenuItem(
value: method,
child: Text(label),
);
}).toList(),
onChanged: (value) {
if (value != null) {
flowProvider.selectMethod(value);
}
},
),
if (hasRecipient && methodsForSelectedType.length > 1)
SizedBox(height: dimensions.paddingMedium),
PaymentMethodForm(
selectedType: selectedType,
onChanged: (data) {
if (!hasRecipient) {
flowProvider.setManualPaymentData(data);
}
},
initialData: flowProvider.selectedPaymentData,
isEditable: !hasRecipient,
),
],
);
}
}

View File

@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pweb/pages/payment_methods/add/widget.dart';
import 'package:pweb/widgets/dialogs/confirmation_dialog.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentConfigController {
final BuildContext context;
PaymentConfigController(this.context);
Future<void> addMethod() async => showDialog<PaymentMethodData>(
context: context,
builder: (_) => const AddPaymentMethodDialog(),
);
Future<void> editMethod(PaymentMethod method) async {
// TODO: implement edit functionality
}
Future<void> deleteMethod(PaymentMethod method) async {
final methodsProvider = context.read<PaymentMethodsProvider>();
final l10n = AppLocalizations.of(context)!;
final confirmed = await showConfirmationDialog(
context: context,
title: l10n.delete,
message: l10n.deletePaymentConfirmation,
confirmLabel: l10n.delete,
);
if (confirmed) {
methodsProvider.delete(method.id);
}
}
void toggleEnabled(PaymentMethod method, bool value) {
context.read<PaymentMethodsProvider>().setArchivedMethod(method: method, newIsArchived: value);
}
void makeMain(PaymentMethod method) {
context.read<PaymentMethodsProvider>().makeMain(method);
}
void reorder(int oldIndex, int newIndex) {
// TODO: rimplement on top of Indexable
// context.read<PaymentMethodsProvider>().reorderMethods(oldIndex, newIndex);
}
}

View File

@@ -5,8 +5,8 @@ import 'package:provider/provider.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/pages/payout_page/methods/widget.dart';
import 'package:pweb/pages/payout_page/wallet/wigets.dart';
import 'package:pweb/pages/payment_methods/manage/widget.dart';
import 'package:pweb/pages/payout_page/wallet/widget.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -6,9 +6,10 @@ import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/pages/payment_methods/widgets/state_view.dart';
import 'package:pweb/pages/payment_methods/payment_page/page.dart';
import 'package:pweb/models/control_state.dart';
import 'package:pweb/pages/payout_page/send/widgets/state_view.dart';
import 'package:pweb/pages/payout_page/send/content.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -16,7 +17,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageBody extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final Recipient? previousRecipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
@@ -31,12 +31,15 @@ class PaymentPageBody extends StatelessWidget {
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onSend;
final VoidCallback onAddRecipient;
final VoidCallback onAddPaymentMethod;
final VisibilityState paymentDetailsVisibility;
final VoidCallback onTogglePaymentDetails;
const PaymentPageBody({
super.key,
required this.onBack,
required this.recipient,
required this.previousRecipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
@@ -51,6 +54,10 @@ class PaymentPageBody extends StatelessWidget {
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onSend,
required this.onAddRecipient,
required this.onAddPaymentMethod,
required this.paymentDetailsVisibility,
required this.onTogglePaymentDetails,
});
@override
@@ -70,7 +77,6 @@ class PaymentPageBody extends StatelessWidget {
return PaymentPageContent(
onBack: onBack,
recipient: recipient,
previousRecipient: previousRecipient,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
@@ -84,6 +90,10 @@ class PaymentPageBody extends StatelessWidget {
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
onSend: onSend,
onAddRecipient: onAddRecipient,
onAddPaymentMethod: onAddPaymentMethod,
paymentDetailsVisibility: paymentDetailsVisibility,
onTogglePaymentDetails: onTogglePaymentDetails,
);
}
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payout_page/send/content/layout.dart';
import 'package:pweb/pages/payout_page/send/content/sections.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageContent extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination;
final ControlState sendState;
final int cooldownRemainingSeconds;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onSend;
final VoidCallback onAddRecipient;
final VoidCallback onAddPaymentMethod;
final VisibilityState paymentDetailsVisibility;
final VoidCallback onTogglePaymentDetails;
const PaymentPageContent({
super.key,
required this.onBack,
required this.recipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.onWalletSelected,
required this.fallbackDestination,
required this.sendState,
required this.cooldownRemainingSeconds,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onSend,
required this.onAddRecipient,
required this.onAddPaymentMethod,
required this.paymentDetailsVisibility,
required this.onTogglePaymentDetails,
});
@override
Widget build(BuildContext context) {
final dimensions = AppDimensions();
final loc = AppLocalizations.of(context)!;
final maxWidth = dimensions.maxContentWidth + 180;
return PaymentPageContentLayout(
maxWidth: maxWidth,
padding: EdgeInsets.only(
left: dimensions.paddingSmall,
right: dimensions.paddingSmall,
bottom: dimensions.paddingLarge,
),
child: PaymentPageContentSections(
dimensions: dimensions,
sourceOfFundsTitle: loc.sourceOfFunds,
onBack: onBack,
recipient: recipient,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
onWalletSelected: onWalletSelected,
fallbackDestination: fallbackDestination,
sendState: sendState,
cooldownRemainingSeconds: cooldownRemainingSeconds,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
onSend: onSend,
onAddRecipient: onAddRecipient,
onAddPaymentMethod: onAddPaymentMethod,
paymentDetailsVisibility: paymentDetailsVisibility,
onTogglePaymentDetails: onTogglePaymentDetails,
),
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/payout_page/send/widgets/back_button.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
class PaymentPageBackSection extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final PayoutDestination fallbackDestination;
const PaymentPageBackSection({
super.key,
required this.onBack,
required this.recipient,
required this.fallbackDestination,
});
@override
Widget build(BuildContext context) {
return PaymentBackButton(
onBack: onBack,
recipient: recipient,
fallbackDestination: fallbackDestination,
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class PaymentPageContentLayout extends StatelessWidget {
final double maxWidth;
final EdgeInsets padding;
final Widget child;
const PaymentPageContentLayout({
super.key,
required this.maxWidth,
required this.padding,
required this.child,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: SingleChildScrollView(
padding: padding,
child: child,
),
),
);
}
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payout_page/send/widgets/recipient_details_card.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/models/state/visibility.dart';
class PaymentPageRecipientSection extends StatelessWidget {
final AppDimensions dimensions;
final Recipient? recipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onAddRecipient;
final VoidCallback onAddPaymentMethod;
final VisibilityState paymentDetailsVisibility;
final VoidCallback onTogglePaymentDetails;
const PaymentPageRecipientSection({
super.key,
required this.dimensions,
required this.recipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onAddRecipient,
required this.onAddPaymentMethod,
required this.paymentDetailsVisibility,
required this.onTogglePaymentDetails,
});
@override
Widget build(BuildContext context) {
return PaymentRecipientDetailsCard(
dimensions: dimensions,
recipient: recipient,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
onAddRecipient: onAddRecipient,
onAddPaymentMethod: onAddPaymentMethod,
paymentDetailsVisibility: paymentDetailsVisibility,
onTogglePaymentDetails: onTogglePaymentDetails,
);
}
}

View File

@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payout_page/send/content/back_section.dart';
import 'package:pweb/pages/payout_page/send/content/recipient_section.dart';
import 'package:pweb/pages/payout_page/send/content/send_section.dart';
import 'package:pweb/pages/payout_page/send/content/source_section.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/models/state/visibility.dart';
class PaymentPageContentSections extends StatelessWidget {
final AppDimensions dimensions;
final String sourceOfFundsTitle;
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination;
final ControlState sendState;
final int cooldownRemainingSeconds;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onSend;
final VoidCallback onAddRecipient;
final VoidCallback onAddPaymentMethod;
final VisibilityState paymentDetailsVisibility;
final VoidCallback onTogglePaymentDetails;
const PaymentPageContentSections({
super.key,
required this.dimensions,
required this.sourceOfFundsTitle,
required this.onBack,
required this.recipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.onWalletSelected,
required this.fallbackDestination,
required this.sendState,
required this.cooldownRemainingSeconds,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onSend,
required this.onAddRecipient,
required this.onAddPaymentMethod,
required this.paymentDetailsVisibility,
required this.onTogglePaymentDetails,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
PaymentPageBackSection(
onBack: onBack,
recipient: recipient,
fallbackDestination: fallbackDestination,
),
SizedBox(height: dimensions.paddingSmall),
PaymentPageSourceSection(
dimensions: dimensions,
title: sourceOfFundsTitle,
onWalletSelected: onWalletSelected,
),
SizedBox(height: dimensions.paddingXLarge),
PaymentPageRecipientSection(
dimensions: dimensions,
recipient: recipient,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
onAddRecipient: onAddRecipient,
onAddPaymentMethod: onAddPaymentMethod,
paymentDetailsVisibility: paymentDetailsVisibility,
onTogglePaymentDetails: onTogglePaymentDetails,
),
SizedBox(height: dimensions.paddingXLarge),
PaymentPageSendSection(
dimensions: dimensions,
sendState: sendState,
cooldownRemainingSeconds: cooldownRemainingSeconds,
onSend: onSend,
),
],
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/payout_page/send/widgets/send_card.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/models/state/control_state.dart';
class PaymentPageSendSection extends StatelessWidget {
final AppDimensions dimensions;
final ControlState sendState;
final int cooldownRemainingSeconds;
final VoidCallback onSend;
const PaymentPageSendSection({
super.key,
required this.dimensions,
required this.sendState,
required this.cooldownRemainingSeconds,
required this.onSend,
});
@override
Widget build(BuildContext context) {
return PaymentSendCard(
dimensions: dimensions,
sendState: sendState,
cooldownRemainingSeconds: cooldownRemainingSeconds,
onSend: onSend,
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/pages/payout_page/send/widgets/source_of_funds_card.dart';
import 'package:pweb/utils/dimensions.dart';
class PaymentPageSourceSection extends StatelessWidget {
final AppDimensions dimensions;
final String title;
final ValueChanged<Wallet> onWalletSelected;
const PaymentPageSourceSection({
super.key,
required this.dimensions,
required this.title,
required this.onWalletSelected,
});
@override
Widget build(BuildContext context) {
return PaymentSourceOfFundsCard(
dimensions: dimensions,
title: title,
onWalletSelected: onWalletSelected,
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/controllers/payments/page_ui.dart';
import 'package:pweb/pages/payout_page/send/page_handlers.dart';
import 'package:pweb/pages/payout_page/send/page_view.dart';
class PaymentPage extends StatefulWidget {
final ValueChanged<Recipient?>? onBack;
final PaymentType? initialPaymentType;
final PayoutDestination fallbackDestination;
const PaymentPage({
super.key,
this.onBack,
this.initialPaymentType,
this.fallbackDestination = PayoutDestination.dashboard,
});
@override
State<PaymentPage> createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> {
late final PaymentPageUiController _uiController;
@override
void initState() {
super.initState();
_uiController = PaymentPageUiController();
WidgetsBinding.instance.addPostFrameCallback(
(_) => initializePaymentPage(context, widget.initialPaymentType),
);
}
@override
void dispose() {
_uiController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return PaymentPageView(
uiController: _uiController,
onBack: widget.onBack,
fallbackDestination: widget.fallbackDestination,
onSearchChanged: (query) => handleSearchChanged(_uiController, query),
onRecipientSelected: (recipient) =>
handleRecipientSelected(context, _uiController, recipient),
onRecipientCleared: () => handleRecipientCleared(context, _uiController),
onSend: () => handleSendPayment(
state: this,
fallbackDestination: widget.fallbackDestination,
),
onAddRecipient: () => handleAddRecipient(context),
onAddPaymentMethod: () => handleAddPaymentMethod(context),
);
}
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/payment/flow.dart';
import 'package:pshared/provider/payment/provider.dart';
import 'package:pshared/provider/payment/quotation/quotation.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
import 'package:pweb/controllers/payments/page.dart';
import 'package:pweb/controllers/payments/page_ui.dart';
import 'package:pweb/controllers/payouts/payout_verification.dart';
import 'package:pweb/utils/payment/payout_verification_flow.dart';
void initializePaymentPage(
BuildContext context,
PaymentType? initialPaymentType,
) {
final flowProvider = context.read<PaymentFlowProvider>();
flowProvider.setPreferredType(initialPaymentType);
}
void handleSearchChanged(PaymentPageUiController uiController, String query) {
uiController.setQuery(query);
}
void handleRecipientSelected(
BuildContext context,
PaymentPageUiController uiController,
Recipient recipient,
) {
final recipientProvider = context.read<RecipientsProvider>();
recipientProvider.setCurrentObject(recipient.id);
uiController.clearSearch();
}
void handleRecipientCleared(
BuildContext context,
PaymentPageUiController uiController,
) {
final recipientProvider = context.read<RecipientsProvider>();
recipientProvider.setCurrentObject(null);
uiController.clearSearch();
}
Future<void> handleSendPayment({
required State state,
required PayoutDestination fallbackDestination,
}) async {
final context = state.context;
final paymentProvider = context.read<PaymentProvider>();
final quotationProvider = context.read<QuotationProvider>();
final controller = context.read<PaymentPageController>();
final verificationController = context.read<PayoutVerificationController>();
if (paymentProvider.isLoading) return;
final verificationContextKey =
quotationProvider.quotation?.quoteRef ??
quotationProvider.quotation?.idempotencyKey;
final verified = await runPayoutVerification(
context: context,
controller: verificationController,
contextKey: verificationContextKey,
);
if (!verified || !state.mounted) return;
final isSuccess = await controller.sendPayment();
if (!state.mounted) return;
await showPaymentStatusDialog(context, isSuccess: isSuccess);
if (!state.mounted) return;
if (isSuccess) {
controller.handleSuccess();
context.goToPayout(fallbackDestination);
}
}
void handleAddRecipient(BuildContext context) {
final recipients = context.read<RecipientsProvider>();
recipients.setCurrentObject(null);
context.pushToPayout(PayoutDestination.addrecipient);
}
void handleAddPaymentMethod(BuildContext context) {
final recipients = context.read<RecipientsProvider>();
final recipient = recipients.currentObject;
if (recipient == null) return;
recipients.setCurrentObject(recipient.id);
context.pushNamed(PayoutRoutes.editRecipient);
}

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/payment/quotation/quotation.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payout_page/send/body.dart';
import 'package:pweb/utils/recipient/filtering.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/controllers/payments/page_ui.dart';
import 'package:pweb/controllers/payouts/payout_verification.dart';
import 'package:pweb/models/state/control_state.dart';
class PaymentPageView extends StatelessWidget {
final PaymentPageUiController uiController;
final ValueChanged<Recipient?>? onBack;
final PayoutDestination fallbackDestination;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onSend;
final VoidCallback onAddRecipient;
final VoidCallback onAddPaymentMethod;
const PaymentPageView({
super.key,
required this.uiController,
required this.onBack,
required this.fallbackDestination,
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onSend,
required this.onAddRecipient,
required this.onAddPaymentMethod,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: uiController,
child: Builder(
builder: (context) {
final uiController = context.watch<PaymentPageUiController>();
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipientProvider = context.watch<RecipientsProvider>();
final quotationProvider = context.watch<QuotationProvider>();
final verificationController =
context.watch<PayoutVerificationController>();
final verificationContextKey =
quotationProvider.quotation?.quoteRef ??
quotationProvider.quotation?.idempotencyKey;
final recipient = recipientProvider.currentObject;
final filteredRecipients = filterRecipients(
recipients: recipientProvider.recipients,
query: uiController.query,
);
final sendState =
verificationController.isCooldownActiveFor(verificationContextKey)
? ControlState.disabled
: (recipient == null
? ControlState.disabled
: ControlState.enabled);
return PaymentPageBody(
onBack: onBack,
fallbackDestination: fallbackDestination,
recipient: recipient,
recipientProvider: recipientProvider,
searchQuery: uiController.query,
filteredRecipients: filteredRecipients,
methodsProvider: methodsProvider,
sendState: sendState,
cooldownRemainingSeconds:
verificationController
.cooldownRemainingSecondsFor(verificationContextKey),
onWalletSelected: context.read<WalletsController>().selectWallet,
searchController: uiController.searchController,
searchFocusNode: uiController.searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
onSend: onSend,
onAddRecipient: onAddRecipient,
onAddPaymentMethod: onAddPaymentMethod,
paymentDetailsVisibility: uiController.paymentDetailsVisibility,
onTogglePaymentDetails: uiController.togglePaymentDetails,
);
},
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
class AddPaymentMethodTile extends StatelessWidget {
final String label;
final VoidCallback onTap;
const AddPaymentMethodTile({
super.key,
required this.label,
required this.onTap,
});
static const double _borderRadius = 12;
static const double _iconSize = 18;
static const double _minWidth = 150;
static const EdgeInsets _padding = EdgeInsets.all(12);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final borderColor = theme.colorScheme.primary.withValues(alpha: 0.45);
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: _minWidth),
child: Material(
color: theme.colorScheme.onSecondary,
borderRadius: BorderRadius.circular(_borderRadius),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(_borderRadius),
child: Container(
padding: _padding,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_borderRadius),
border: Border.all(color: borderColor),
),
child: Row(
children: [
Icon(Icons.add, size: _iconSize, color: theme.colorScheme.primary),
const SizedBox(width: 8),
Expanded(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.primary,
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
class AddRecipientTile extends StatelessWidget {
final String label;
final VoidCallback onTap;
const AddRecipientTile({
super.key,
required this.label,
required this.onTap,
});
static const double _avatarRadius = 20;
static const double _tileSize = 80;
static const double _verticalSpacing = 6;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
hoverColor: theme.colorScheme.primaryContainer,
child: SizedBox(
width: _tileSize,
height: _tileSize,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: _avatarRadius,
backgroundColor: theme.colorScheme.primaryContainer,
child: Icon(
Icons.add,
color: theme.colorScheme.primary,
size: 20,
),
),
const SizedBox(height: _verticalSpacing),
Text(
label,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(fontSize: 12),
),
],
),
),
);
}
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/pages/payout_page/send/widgets/section/title.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -0,0 +1,10 @@
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/utils/payment/label.dart';
String? buildPaymentInfoDetailsText(RecipientMethodDraft entry) {
final method = entry.existing;
if (method == null) return null;
return getPaymentMethodMaskedValue(method);
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/payout_page/send/widgets/section/title.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/utils/dimensions.dart';
class PaymentInfoHeader extends StatelessWidget {
final AppDimensions dimensions;
final String title;
final VisibilityState visibility;
const PaymentInfoHeader({
super.key,
required this.dimensions,
required this.title,
required this.visibility,
});
@override
Widget build(BuildContext context) {
if (visibility != VisibilityState.visible) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(title),
SizedBox(height: dimensions.paddingSmall),
],
);
}
}

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/selector_row.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/details_builder.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_state.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/utils/payment/availability.dart';
class PaymentInfoMethodsSection extends StatelessWidget {
final AppDimensions dimensions;
final String title;
final VisibilityState titleVisibility;
final String detailsLabel;
final PaymentInfoMethodsState state;
final VoidCallback? onAddMethod;
final VisibilityState paymentDetailsVisibility;
final VoidCallback onTogglePaymentDetails;
final ValueChanged<RecipientMethodDraft> onEntrySelected;
const PaymentInfoMethodsSection({
super.key,
required this.dimensions,
required this.title,
required this.titleVisibility,
required this.detailsLabel,
required this.state,
required this.onAddMethod,
required this.paymentDetailsVisibility,
required this.onTogglePaymentDetails,
required this.onEntrySelected,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PaymentInfoHeader(
dimensions: dimensions,
title: title,
visibility: titleVisibility,
),
PaymentMethodSelectorRow(
types: state.types,
selectedType: state.selectedType,
selectedIndex: state.hasSelection ? state.selectedIndex : null,
methods: state.methodsMap,
detailsBuilder: buildPaymentInfoDetailsText,
onSelected: _handleSelected,
onAddPressed: onAddMethod,
disabledTypes: disabledPaymentTypes,
),
if (state.hasSelection) ...[
SizedBox(height: dimensions.paddingSmall),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: onTogglePaymentDetails,
icon: Icon(
paymentDetailsVisibility == VisibilityState.visible
? Icons.expand_less
: Icons.expand_more,
),
label: Text(detailsLabel),
),
),
if (paymentDetailsVisibility == VisibilityState.visible) ...[
SizedBox(height: dimensions.paddingSmall),
PaymentMethodPanel(
selectedType: state.selectedType,
selectedIndex: state.selectedIndex!,
entries: state.selectedEntries,
onRemove: (_) {},
onChanged: (_, _) {},
editState: ControlState.disabled,
deleteVisibility: VisibilityState.hidden,
),
],
],
],
);
}
void _handleSelected(PaymentType type, int index) {
final entries = state.methodsMap[type] ?? const <RecipientMethodDraft>[];
if (index < 0 || index >= entries.length) return;
onEntrySelected(entries[index]);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pshared/provider/payment/flow.dart';
class PaymentInfoMethodsState {
final List<PaymentType> types;
final Map<PaymentType, List<RecipientMethodDraft>> methodsMap;
final PaymentType selectedType;
final List<RecipientMethodDraft> selectedEntries;
final int? selectedIndex;
const PaymentInfoMethodsState({
required this.types,
required this.methodsMap,
required this.selectedType,
required this.selectedEntries,
required this.selectedIndex,
});
bool get hasSelection => selectedIndex != null && selectedIndex! >= 0;
}
PaymentInfoMethodsState buildPaymentInfoMethodsState({
required PaymentFlowProvider flowProvider,
required List<PaymentType> types,
}) {
final methods = flowProvider.methodsForRecipient;
final selectedMethod = flowProvider.selectedMethod;
final methodsMap = <PaymentType, List<RecipientMethodDraft>>{};
for (final method in methods) {
methodsMap.putIfAbsent(method.type, () => []).add(
RecipientMethodDraft(
type: method.type,
existing: method,
),
);
}
final fallbackType = methods.isNotEmpty
? methods.first.type
: (types.isNotEmpty ? types.first : PaymentType.bankAccount);
final selectedType = selectedMethod?.type ?? fallbackType;
final selectedEntries =
methodsMap[selectedType] ?? const <RecipientMethodDraft>[];
final selectedIndex = selectedMethod == null
? null
: selectedEntries.indexWhere(
(entry) => entry.existing?.id == selectedMethod.id,
);
return PaymentInfoMethodsState(
types: types,
methodsMap: methodsMap,
selectedType: selectedType,
selectedEntries: selectedEntries,
selectedIndex: selectedIndex,
);
}

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/selector_row.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/utils/payment/availability.dart';
class PaymentInfoNoMethodsSection extends StatelessWidget {
final AppDimensions dimensions;
final String title;
final VisibilityState titleVisibility;
final String emptyMessage;
final List<PaymentType> types;
final VoidCallback? onAddMethod;
const PaymentInfoNoMethodsSection({
super.key,
required this.dimensions,
required this.title,
required this.titleVisibility,
required this.emptyMessage,
required this.types,
required this.onAddMethod,
});
@override
Widget build(BuildContext context) {
final fallbackType = types.isNotEmpty ? types.first : PaymentType.bankAccount;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PaymentInfoHeader(
dimensions: dimensions,
title: title,
visibility: titleVisibility,
),
Text(emptyMessage),
if (onAddMethod != null) ...[
SizedBox(height: dimensions.paddingMedium),
PaymentMethodSelectorRow(
types: types,
selectedType: fallbackType,
selectedIndex: null,
methods: const {},
onSelected: (_, _) {},
onAddPressed: onAddMethod,
disabledTypes: disabledPaymentTypes,
),
],
],
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/utils/dimensions.dart';
class PaymentInfoNoRecipientSection extends StatelessWidget {
final AppDimensions dimensions;
final String title;
final VisibilityState titleVisibility;
const PaymentInfoNoRecipientSection({
super.key,
required this.dimensions,
required this.title,
required this.titleVisibility,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PaymentInfoHeader(
dimensions: dimensions,
title: title,
visibility: titleVisibility,
),
],
);
}
}

View File

@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/flow.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_section.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_state.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_methods.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_recipient.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/utils/payment/availability.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentInfoSection extends StatelessWidget {
final AppDimensions dimensions;
final VisibilityState titleVisibility;
final VoidCallback? onAddMethod;
final VisibilityState paymentDetailsVisibility;
final VoidCallback onTogglePaymentDetails;
const PaymentInfoSection({
super.key,
required this.dimensions,
this.titleVisibility = VisibilityState.visible,
this.onAddMethod,
required this.paymentDetailsVisibility,
required this.onTogglePaymentDetails,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final flowProvider = context.watch<PaymentFlowProvider>();
if (!flowProvider.hasRecipient) {
return PaymentInfoNoRecipientSection(
dimensions: dimensions,
title: loc.paymentInfo,
titleVisibility: titleVisibility,
);
}
final methods = flowProvider.methodsForRecipient;
final types = visiblePaymentTypes;
if (methods.isEmpty) {
return PaymentInfoNoMethodsSection(
dimensions: dimensions,
title: loc.paymentInfo,
titleVisibility: titleVisibility,
emptyMessage: loc.recipientNoPaymentDetails,
types: types,
onAddMethod: onAddMethod,
);
}
final state = buildPaymentInfoMethodsState(
flowProvider: flowProvider,
types: types,
);
return PaymentInfoMethodsSection(
dimensions: dimensions,
title: loc.paymentInfo,
titleVisibility: titleVisibility,
detailsLabel: loc.paymentMethodDetails,
state: state,
onAddMethod: onAddMethod,
paymentDetailsVisibility: paymentDetailsVisibility,
onTogglePaymentDetails: onTogglePaymentDetails,
onEntrySelected: (entry) {
final existing = entry.existing;
if (existing != null) {
flowProvider.selectMethod(existing);
}
},
);
}
}

View File

@@ -0,0 +1 @@
export 'package:pweb/pages/payout_page/send/widgets/payment_info/section.dart';

View File

@@ -4,9 +4,11 @@ import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/address_book/page/search.dart';
import 'package:pweb/pages/payment_methods/widgets/card.dart';
import 'package:pweb/pages/payment_methods/widgets/search.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/pages/payout_page/send/widgets/card.dart';
import 'package:pweb/pages/payout_page/send/widgets/search.dart';
import 'package:pweb/pages/payout_page/send/widgets/section/title.dart';
import 'package:pweb/pages/payout_page/send/widgets/add_recipient_tile.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/short_list.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -14,7 +16,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class RecipientSection extends StatelessWidget {
final Recipient? recipient;
final Recipient? previousRecipient;
final AppDimensions dimensions;
final RecipientsProvider recipientProvider;
final String searchQuery;
@@ -24,11 +25,11 @@ class RecipientSection extends StatelessWidget {
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onAddRecipient;
const RecipientSection({
super.key,
required this.recipient,
required this.previousRecipient,
required this.dimensions,
required this.recipientProvider,
required this.searchQuery,
@@ -38,6 +39,7 @@ class RecipientSection extends StatelessWidget {
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onAddRecipient,
});
@override
@@ -55,7 +57,6 @@ class RecipientSection extends StatelessWidget {
animation: recipientProvider,
builder: (context, _) {
final hasQuery = searchQuery.isNotEmpty;
final prev = previousRecipient;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -67,17 +68,6 @@ class RecipientSection extends StatelessWidget {
onChanged: onSearchChanged,
focusNode: searchFocusNode,
),
if (prev != null) ...[
SizedBox(height: dimensions.paddingSmall),
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.undo),
title: Text(loc.back),
subtitle: Text(prev.name),
onTap: () => onRecipientSelected(prev),
),
],
if (hasQuery) ...[
SizedBox(height: dimensions.paddingMedium),
RecipientSearchResults(
@@ -86,6 +76,16 @@ class RecipientSection extends StatelessWidget {
results: filteredRecipients,
onRecipientSelected: onRecipientSelected,
),
] else ...[
SizedBox(height: dimensions.paddingMedium),
ShortListAddressBookPayout(
recipients: recipientProvider.recipients,
onSelected: onRecipientSelected,
trailing: AddRecipientTile(
label: loc.addRecipient,
onTap: onAddRecipient,
),
),
],
],
);

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/section.dart';
import 'package:pweb/pages/payout_page/send/widgets/recipient/section.dart';
import 'package:pweb/pages/payout_page/send/widgets/section/card.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/models/state/visibility.dart';
class PaymentRecipientDetailsCard extends StatelessWidget {
final AppDimensions dimensions;
final Recipient? recipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onAddRecipient;
final VoidCallback onAddPaymentMethod;
final VisibilityState paymentDetailsVisibility;
final VoidCallback onTogglePaymentDetails;
const PaymentRecipientDetailsCard({
super.key,
required this.dimensions,
required this.recipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onAddRecipient,
required this.onAddPaymentMethod,
required this.paymentDetailsVisibility,
required this.onTogglePaymentDetails,
});
@override
Widget build(BuildContext context) {
return PaymentSectionCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RecipientSection(
recipient: recipient,
dimensions: dimensions,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
onAddRecipient: onAddRecipient,
),
SizedBox(height: dimensions.paddingMedium),
PaymentInfoSection(
dimensions: dimensions,
titleVisibility: VisibilityState.hidden,
onAddMethod: onAddPaymentMethod,
paymentDetailsVisibility: paymentDetailsVisibility,
onTogglePaymentDetails: onTogglePaymentDetails,
),
],
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:pweb/utils/dimensions.dart';
class PaymentSectionCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
const PaymentSectionCard({
super.key,
required this.child,
this.padding,
});
@override
Widget build(BuildContext context) {
final dimensions = AppDimensions();
final theme = Theme.of(context);
return Material(
elevation: dimensions.elevationSmall,
borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium),
color: theme.colorScheme.onSecondary,
clipBehavior: Clip.antiAlias,
child: Padding(
padding: padding ?? EdgeInsets.all(dimensions.paddingLarge),
child: child,
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pweb/models/control_state.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/dashboard/payouts/form.dart';
import 'package:pweb/pages/payout_page/send/widgets/send_button.dart';
import 'package:pweb/pages/payout_page/send/widgets/section/card.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/widgets/cooldown_hint.dart';
import 'package:pweb/models/state/control_state.dart';
class PaymentSendCard extends StatelessWidget {
final AppDimensions dimensions;
final ControlState sendState;
final int cooldownRemainingSeconds;
final VoidCallback onSend;
const PaymentSendCard({
super.key,
required this.dimensions,
required this.sendState,
required this.cooldownRemainingSeconds,
required this.onSend,
});
@override
Widget build(BuildContext context) {
return PaymentSectionCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const PaymentFormWidget(),
SizedBox(height: dimensions.paddingXLarge),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SendButton(
onPressed: onSend,
state: sendState,
),
if (sendState == ControlState.disabled &&
cooldownRemainingSeconds > 0) ...[
SizedBox(height: dimensions.paddingSmall),
CooldownHint(seconds: cooldownRemainingSeconds),
],
],
),
],
),
);
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/pages/payout_page/send/widgets/method_selector.dart';
import 'package:pweb/pages/payout_page/send/widgets/section/title.dart';
import 'package:pweb/pages/payout_page/send/widgets/section/card.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/widgets/refresh_balance/wallet.dart';
class PaymentSourceOfFundsCard extends StatelessWidget {
final AppDimensions dimensions;
final String title;
final ValueChanged<Wallet> onWalletSelected;
const PaymentSourceOfFundsCard({
super.key,
required this.dimensions,
required this.title,
required this.onWalletSelected,
});
@override
Widget build(BuildContext context) {
return PaymentSectionCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(child: SectionTitle(title)),
Consumer<WalletsController>(
builder: (context, provider, _) {
final selectedWalletId = provider.selectedWallet?.id;
if (selectedWalletId == null) {
return const SizedBox.shrink();
}
return WalletBalanceRefreshButton(walletRef: selectedWalletId);
},
),
],
),
SizedBox(height: dimensions.paddingSmall),
PaymentMethodSelector(
onMethodChanged: onWalletSelected,
),
],
),
);
}
}

View File

@@ -6,7 +6,7 @@ import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/models/visibility.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
import 'package:pweb/widgets/refresh_balance/wallet.dart';

View File

@@ -1,12 +1,13 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -27,9 +28,11 @@ class SendPayoutButton extends StatelessWidget {
final wallet = wallets.selectedWallet;
if (wallet != null) {
context.pushToPayment(
paymentType: PaymentType.wallet,
returnTo: PayoutDestination.editwallet,
context.pushNamed(
PayoutRoutes.payment,
queryParameters: PayoutRoutes.buildQueryParameters(
paymentType: PaymentType.wallet,
),
);
}
},

View File

@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -29,7 +28,7 @@ class TopUpButton extends StatelessWidget{
);
return;
}
context.pushToWalletTopUp(returnTo: PayoutDestination.editwallet);
context.pushToWalletTopUp();
},
child: Text(loc.topUpBalance),
);

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:pweb/models/wallet_transaction.dart';
import 'package:pweb/models/wallet/wallet_transaction.dart';
class TypeChip extends StatelessWidget {

View File

@@ -3,14 +3,14 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pshared/utils/localization.dart';
import 'package:pweb/models/wallet_transaction.dart';
import 'package:pweb/providers/wallet_transactions.dart';
import 'package:pweb/models/wallet/wallet_transaction.dart';
import 'package:pweb/controllers/operations/wallet_transactions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletHistoryFilters extends StatelessWidget {
final WalletTransactionsProvider provider;
final WalletTransactionsController provider;
final VoidCallback onPickRange;
const WalletHistoryFilters({

View File

@@ -6,6 +6,7 @@ import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/pages/payout_page/wallet/history/filters.dart';
import 'package:pweb/pages/payout_page/wallet/history/table.dart';
import 'package:pweb/controllers/operations/wallet_transactions.dart';
import 'package:pweb/providers/wallet_transactions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -44,7 +45,7 @@ class _WalletHistoryState extends State<WalletHistory> {
}
Future<void> _pickRange() async {
final provider = context.read<WalletTransactionsProvider>();
final provider = context.read<WalletTransactionsController>();
final now = DateTime.now();
final initial = provider.dateRange ??
DateTimeRange(
@@ -69,7 +70,7 @@ class _WalletHistoryState extends State<WalletHistory> {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
return Consumer<WalletTransactionsProvider>(
return Consumer<WalletTransactionsController>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Padding(

Some files were not shown because too many files have changed in this diff Show More