redesigned payment page + a lot of fixes

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

View File

@@ -1,20 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentConfigAdvanced extends StatelessWidget {
const PaymentConfigAdvanced({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ExpansionTile(
title: Text(l10n.advanced),
tilePadding: const EdgeInsets.symmetric(horizontal: 16),
childrenPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [Text(l10n.fallbackExplanation)],
);
}
}

View File

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

View File

@@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentConfigHeader extends StatelessWidget {
final VoidCallback onAdd;
const PaymentConfigHeader({super.key, required this.onAdd});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
return Column(
children: [
Text(
l10n.paymentConfigTitle,
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(l10n.paymentConfigSubtitle, textAlign: TextAlign.center),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
icon: const Icon(Icons.add),
label: Text(l10n.addPaymentMethod),
onPressed: onAdd,
),
),
],
);
}
}

View File

@@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
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';
class PaymentConfigList extends StatelessWidget {
final PaymentConfigController controller;
const PaymentConfigList({super.key, required this.controller});
@override
Widget build(BuildContext context) {
final provider = context.watch<PaymentMethodsProvider>();
return ReorderableListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: provider.methods.length,
onReorder: controller.reorder,
itemBuilder: (context, index) {
final method = provider.methods[index];
return ReorderableDragStartListener(
key: Key(method.id),
index: index,
child: PaymentMethodTile(
method: method,
index: index,
makeMain: () => controller.makeMain(method),
toggleEnabled: (v) => controller.toggleEnabled(method, v),
edit: () => controller.editMethod(method),
delete: () => controller.deleteMethod(method),
),
);
},
);
}
}

View File

@@ -1,49 +0,0 @@
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';
class MethodsWidget extends StatefulWidget {
const MethodsWidget({super.key});
@override
State<MethodsWidget> createState() => _MethodsWidgetState();
}
class _MethodsWidgetState extends State<MethodsWidget> {
late final PaymentConfigController controller;
@override
void initState() {
super.initState();
controller = PaymentConfigController(context);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: theme.cardTheme.elevation ?? 4,
color: theme.colorScheme.onSecondary,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
PaymentConfigHeader(onAdd: controller.addMethod),
const SizedBox(height: 12),
PaymentConfigList(controller: controller),
const SizedBox(height: 12),
const PaymentConfigAdvanced(),
],
),
),
),
);
}
}

View File

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

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/payment/wallet.dart';
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/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';
class PaymentPageBody extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final PaymentMethodsProvider methodsProvider;
final ControlState sendState;
final int cooldownRemainingSeconds;
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;
final VoidCallback onAddRecipient;
final VoidCallback onAddPaymentMethod;
final VisibilityState paymentDetailsVisibility;
final VoidCallback onTogglePaymentDetails;
const PaymentPageBody({
super.key,
required this.onBack,
required this.recipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.methodsProvider,
required this.sendState,
required this.cooldownRemainingSeconds,
required this.onWalletSelected,
required this.fallbackDestination,
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 loc = AppLocalizations.of(context)!;
if (methodsProvider.isLoading) {
return const PaymentMethodsLoadingView();
}
if (methodsProvider.error != null) {
return PaymentMethodsErrorView(
message: loc.notificationError(methodsProvider.error ?? loc.noErrorInformation),
);
}
return PaymentPageContent(
onBack: onBack,
recipient: recipient,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
onWalletSelected: onWalletSelected,
fallbackDestination: fallbackDestination,
sendState: sendState,
cooldownRemainingSeconds: cooldownRemainingSeconds,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
onSend: onSend,
onAddRecipient: onAddRecipient,
onAddPaymentMethod: onAddPaymentMethod,
paymentDetailsVisibility: paymentDetailsVisibility,
onTogglePaymentDetails: onTogglePaymentDetails,
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
class PaymentBackButton extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final PayoutDestination fallbackDestination;
const PaymentBackButton({
super.key,
required this.onBack,
required this.recipient,
required this.fallbackDestination,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topLeft,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (onBack != null) {
onBack!(recipient);
} else {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
context.goToPayout(fallbackDestination);
}
}
},
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.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';
class SelectedRecipientCard extends StatelessWidget {
final AppDimensions dimensions;
final Recipient recipient;
final VoidCallback onClear;
const SelectedRecipientCard({
super.key,
required this.dimensions,
required this.recipient,
required this.onClear,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final theme = Theme.of(context);
return Container(
width: double.infinity,
padding: EdgeInsets.all(dimensions.paddingMedium),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(loc.recipient),
SizedBox(height: dimensions.paddingSmall),
Row(
children: [
CircleAvatar(
child: Text(recipient.name.substring(0, 1).toUpperCase()),
),
SizedBox(width: dimensions.paddingMedium),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(recipient.name, style: theme.textTheme.titleMedium),
if (recipient.email.isNotEmpty)
Text(
recipient.email,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
],
),
),
TextButton(
onPressed: onClear,
child: Text(loc.chooseAnotherRecipient),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,26 @@
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/widgets/payment/source_wallet_selector.dart';
class PaymentMethodSelector extends StatelessWidget {
final ValueChanged<Wallet> onMethodChanged;
const PaymentMethodSelector({
super.key,
required this.onMethodChanged,
});
@override
Widget build(BuildContext context) => Consumer<WalletsController>(
builder: (context, provider, _) => SourceWalletSelector(
walletsController: provider,
onChanged: onMethodChanged,
),
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
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/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';
class RecipientSection extends StatelessWidget {
final Recipient? recipient;
final AppDimensions dimensions;
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;
const RecipientSection({
super.key,
required this.recipient,
required this.dimensions,
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,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
if (recipient != null) {
return SelectedRecipientCard(
dimensions: dimensions,
recipient: recipient!,
onClear: onRecipientCleared,
);
}
return AnimatedBuilder(
animation: recipientProvider,
builder: (context, _) {
final hasQuery = searchQuery.isNotEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(loc.recipient),
SizedBox(height: dimensions.paddingSmall),
RecipientSearchField(
controller: searchController,
onChanged: onSearchChanged,
focusNode: searchFocusNode,
),
if (hasQuery) ...[
SizedBox(height: dimensions.paddingMedium),
RecipientSearchResults(
dimensions: dimensions,
recipientProvider: recipientProvider,
results: filteredRecipients,
onRecipientSelected: onRecipientSelected,
),
] else ...[
SizedBox(height: dimensions.paddingMedium),
ShortListAddressBookPayout(
recipients: recipientProvider.recipients,
onSelected: onRecipientSelected,
trailing: AddRecipientTile(
label: loc.addRecipient,
onTap: onAddRecipient,
),
),
],
],
);
},
);
}
}

View File

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

View File

@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class RecipientSearchResults extends StatelessWidget {
final AppDimensions dimensions;
final RecipientsProvider recipientProvider;
final List<Recipient> results;
final ValueChanged<Recipient> onRecipientSelected;
const RecipientSearchResults({
super.key,
required this.dimensions,
required this.recipientProvider,
required this.results,
required this.onRecipientSelected,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
if (recipientProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (recipientProvider.error != null) {
return Text(
loc.notificationError(recipientProvider.error ?? loc.noErrorInformation),
style: TextStyle(color: Theme.of(context).colorScheme.error),
);
}
if (recipientProvider.recipients.isEmpty) {
return Text(loc.noRecipientsYet);
}
if (results.isEmpty) {
return Text(loc.noRecipientsFound);
}
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240),
child: ListView.separated(
shrinkWrap: true,
itemCount: results.length,
separatorBuilder: (_, _) => SizedBox(height: dimensions.paddingSmall),
itemBuilder: (context, index) {
final recipient = results[index];
return ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
),
leading: CircleAvatar(
child: Text(recipient.name.substring(0, 1).toUpperCase()),
),
title: Text(recipient.name),
subtitle: Text(recipient.email),
trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16),
onTap: () => onRecipientSelected(recipient),
);
},
),
);
}
}

View File

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

View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class SectionTitle extends StatelessWidget {
final String title;
const SectionTitle(this.title, {super.key});
@override
Widget build(BuildContext context) => Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
);
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class SendButton extends StatelessWidget {
final VoidCallback onPressed;
final ControlState state;
const SendButton({
super.key,
required this.onPressed,
this.state = ControlState.enabled,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dimensions = AppDimensions();
final isEnabled = state == ControlState.enabled;
final isLoading = state == ControlState.loading;
final backgroundColor = isEnabled || isLoading
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withValues(alpha: 0.12);
final textColor = isEnabled || isLoading
? theme.colorScheme.onSecondary
: theme.colorScheme.onSurface.withValues(alpha: 0.38);
return Center(
child: SizedBox(
width: dimensions.buttonWidth,
height: dimensions.buttonHeight,
child: InkWell(
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
onTap: isEnabled ? onPressed : null,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
),
child: Center(
child: isLoading
? SizedBox(
width: dimensions.iconSizeMedium,
height: dimensions.iconSizeMedium,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(textColor),
),
)
: Text(
AppLocalizations.of(context)!.send,
style: theme.textTheme.bodyLarge?.copyWith(
color: textColor,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class PaymentMethodsLoadingView extends StatelessWidget {
const PaymentMethodsLoadingView({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: CircularProgressIndicator());
}
}
class PaymentMethodsErrorView extends StatelessWidget {
final String message;
const PaymentMethodsErrorView({super.key, required this.message});
@override
Widget build(BuildContext context) {
return Center(child: Text(message));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/models/wallet_transaction.dart';
import 'package:pweb/models/wallet/wallet_transaction.dart';
import 'package:pweb/pages/payout_page/wallet/history/chip.dart';
import 'package:pweb/pages/report/table/badge.dart';