redesigned payment page + a lot of fixes
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class _RecipientAddressBookItemState extends State<RecipientAddressBookItem> {
|
||||
child: Padding(
|
||||
padding: widget.padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
37
frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart
Normal file
37
frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
28
frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart
Normal file
28
frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ class RecipientAvatar extends StatelessWidget {
|
||||
final textColor = Theme.of(context).colorScheme.onPrimary;
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: avatarRadius,
|
||||
|
||||
@@ -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)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
frontend/pweb/lib/pages/invitations/page/page.dart
Normal file
75
frontend/pweb/lib/pages/invitations/page/page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
frontend/pweb/lib/pages/invitations/page/providers.dart
Normal file
38
frontend/pweb/lib/pages/invitations/page/providers.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
79
frontend/pweb/lib/pages/invitations/page/view.dart
Normal file
79
frontend/pweb/lib/pages/invitations/page/view.dart
Normal 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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
99
frontend/pweb/lib/pages/payout_page/send/content.dart
Normal file
99
frontend/pweb/lib/pages/payout_page/send/content.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
29
frontend/pweb/lib/pages/payout_page/send/content/layout.dart
Normal file
29
frontend/pweb/lib/pages/payout_page/send/content/layout.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
109
frontend/pweb/lib/pages/payout_page/send/content/sections.dart
Normal file
109
frontend/pweb/lib/pages/payout_page/send/content/sections.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
65
frontend/pweb/lib/pages/payout_page/send/page.dart
Normal file
65
frontend/pweb/lib/pages/payout_page/send/page.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
99
frontend/pweb/lib/pages/payout_page/send/page_handlers.dart
Normal file
99
frontend/pweb/lib/pages/payout_page/send/page_handlers.dart
Normal 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);
|
||||
}
|
||||
98
frontend/pweb/lib/pages/payout_page/send/page_view.dart
Normal file
98
frontend/pweb/lib/pages/payout_page/send/page_view.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export 'package:pweb/pages/payout_page/send/widgets/payment_info/section.dart';
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user