redesigned payment page + a lot of fixes
This commit is contained in:
99
frontend/pweb/lib/pages/payout_page/send/body.dart
Normal file
99
frontend/pweb/lib/pages/payout_page/send/body.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
67
frontend/pweb/lib/pages/payout_page/send/widgets/card.dart
Normal file
67
frontend/pweb/lib/pages/payout_page/send/widgets/card.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
71
frontend/pweb/lib/pages/payout_page/send/widgets/search.dart
Normal file
71
frontend/pweb/lib/pages/payout_page/send/widgets/search.dart
Normal 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),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user