Navigation now flows entirely through go_router

This commit is contained in:
Arseni
2025-12-08 17:40:25 +03:00
parent f478219990
commit 64ad8c8b38
21 changed files with 705 additions and 550 deletions

View File

@@ -1,25 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
Future<bool> showDeleteConfirmationDialog(BuildContext context) async {
final l10n = AppLocalizations.of(context)!;
return await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text(l10n.delete),
content: Text(l10n.deletePaymentConfirmation),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(l10n.cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(l10n.delete),
),
],
),
) ?? false;
}

View File

@@ -1,19 +1,34 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.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/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/providers/payment_flow_provider.dart';
import 'package:pweb/pages/payment_methods/widgets/payment_page_body.dart';
import 'package:pweb/providers/page_selector.dart';
import 'package:pweb/models/wallet.dart';
import 'package:pweb/providers/payment_flow.dart';
import 'package:pweb/pages/payment_methods/payment_page/body.dart';
import 'package:pweb/providers/wallets.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
class PaymentPage extends StatefulWidget {
final ValueChanged<Recipient?>? onBack;
final PaymentType? initialPaymentType;
final PayoutDestination fallbackDestination;
const PaymentPage({super.key, this.onBack});
const PaymentPage({
super.key,
this.onBack,
this.initialPaymentType,
this.fallbackDestination = PayoutDestination.dashboard,
});
@override
State<PaymentPage> createState() => _PaymentPageState();
@@ -29,9 +44,8 @@ class _PaymentPageState extends State<PaymentPage> {
super.initState();
_searchController = TextEditingController();
_searchFocusNode = FocusNode();
final pageSelector = context.read<PageSelectorProvider>();
_flowProvider = PaymentFlowProvider(
initialType: pageSelector.getDefaultPaymentType(),
initialType: widget.initialPaymentType ?? PaymentType.bankAccount,
);
WidgetsBinding.instance.addPostFrameCallback((_) => _initializePaymentPage());
@@ -46,11 +60,15 @@ class _PaymentPageState extends State<PaymentPage> {
}
void _initializePaymentPage() {
final pageSelector = context.read<PageSelectorProvider>();
final methodsProvider = context.read<PaymentMethodsProvider>();
_handleWalletAutoSelection(methodsProvider);
pageSelector.handleWalletAutoSelection();
_flowProvider.syncWithSelector(pageSelector);
final recipient = context.read<RecipientsProvider>().currentObject;
_syncFlowProvider(
recipient: recipient,
methodsProvider: methodsProvider,
preferredType: widget.initialPaymentType,
);
}
void _handleSearchChanged(String query) {
@@ -58,22 +76,28 @@ class _PaymentPageState extends State<PaymentPage> {
}
void _handleRecipientSelected(Recipient recipient) {
final pageSelector = context.read<PageSelectorProvider>();
final recipientProvider = context.read<RecipientsProvider>();
final methodsProvider = context.read<PaymentMethodsProvider>();
recipientProvider.setCurrentObject(recipient.id);
pageSelector.selectRecipient(context, recipient);
_flowProvider.reset(pageSelector);
_flowProvider.reset(
recipient: recipient,
availableTypes: _availablePaymentTypes(recipient, methodsProvider),
preferredType: widget.initialPaymentType,
);
_clearSearchField();
}
void _handleRecipientCleared() {
final pageSelector = context.read<PageSelectorProvider>();
final recipientProvider = context.read<RecipientsProvider>();
final methodsProvider = context.read<PaymentMethodsProvider>();
recipientProvider.setCurrentObject(null);
pageSelector.selectRecipient(context, null);
_flowProvider.reset(pageSelector);
_flowProvider.reset(
recipient: null,
availableTypes: _availablePaymentTypes(null, methodsProvider),
preferredType: widget.initialPaymentType,
);
_clearSearchField();
}
@@ -90,13 +114,26 @@ class _PaymentPageState extends State<PaymentPage> {
@override
Widget build(BuildContext context) {
final pageSelector = context.watch<PageSelectorProvider>();
_flowProvider.syncWithSelector(pageSelector);
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipientProvider = context.watch<RecipientsProvider>();
final recipient = recipientProvider.currentObject;
final availableTypes = _availablePaymentTypes(recipient, methodsProvider);
_syncFlowProvider(
recipient: recipient,
methodsProvider: methodsProvider,
preferredType: recipient != null ? widget.initialPaymentType : null,
);
return ChangeNotifierProvider.value(
value: _flowProvider,
child: PaymentPageBody(
onBack: widget.onBack,
fallbackDestination: widget.fallbackDestination,
recipient: recipient,
recipientProvider: recipientProvider,
methodsProvider: methodsProvider,
availablePaymentTypes: availableTypes,
searchController: _searchController,
searchFocusNode: _searchFocusNode,
onSearchChanged: _handleSearchChanged,
@@ -106,4 +143,56 @@ class _PaymentPageState extends State<PaymentPage> {
),
);
}
}
void _handleWalletAutoSelection(PaymentMethodsProvider methodsProvider) {
final wallet = context.read<WalletsProvider>().selectedWallet;
if (wallet == null) return;
final matchingMethod = _getPaymentMethodForWallet(wallet, methodsProvider);
if (matchingMethod != null) {
methodsProvider.setCurrentObject(matchingMethod.id);
}
}
void _syncFlowProvider({
required Recipient? recipient,
required PaymentMethodsProvider methodsProvider,
PaymentType? preferredType,
}) {
_flowProvider.sync(
recipient: recipient,
availableTypes: _availablePaymentTypes(recipient, methodsProvider),
preferredType: preferredType,
);
}
MethodMap _availablePaymentTypes(
Recipient? recipient,
PaymentMethodsProvider methodsProvider,
) {
if (recipient == null || !methodsProvider.isReady) return {};
final methodsForRecipient = methodsProvider.methods.where(
(method) => !method.isArchived && method.recipientRef == recipient.id,
);
return {
for (final method in methodsForRecipient) method.type: method.data,
};
}
PaymentMethod? _getPaymentMethodForWallet(
Wallet wallet,
PaymentMethodsProvider methodsProvider,
) {
if (methodsProvider.methods.isEmpty) {
return null;
}
return methodsProvider.methods.firstWhereOrNull(
(method) =>
method.type == PaymentType.wallet &&
(method.description?.contains(wallet.walletUserID) ?? false),
);
}
}

View File

@@ -1,33 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
class PaymentDetailsSection extends StatelessWidget {
final bool isFormVisible;
final VoidCallback? onToggle;
final PaymentType selectedType;
final Object? data;
final bool isEditable;
const PaymentDetailsSection({
super.key,
required this.isFormVisible,
this.onToggle,
required this.selectedType,
required this.data,
required this.isEditable,
});
@override
Widget build(BuildContext context) {
return PaymentDetailsSection(
isFormVisible: isFormVisible,
onToggle: onToggle,
selectedType: selectedType,
data: data,
isEditable: isEditable,
);
}
}

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,74 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/recipient/recipient.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/payment_methods/widgets/state_view.dart';
import 'package:pweb/pages/payment_methods/payment_page/page.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageBody extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final RecipientsProvider recipientProvider;
final PaymentMethodsProvider methodsProvider;
final MethodMap availablePaymentTypes;
final PayoutDestination fallbackDestination;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onSend;
const PaymentPageBody({
super.key,
required this.onBack,
required this.recipient,
required this.recipientProvider,
required this.methodsProvider,
required this.availablePaymentTypes,
required this.fallbackDestination,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onSend,
});
@override
Widget build(BuildContext context) {
final 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,
methodsProvider: methodsProvider,
availablePaymentTypes: availablePaymentTypes,
fallbackDestination: fallbackDestination,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
onSend: onSend,
);
}
}

View File

@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payment_methods/back_button.dart';
import 'package:pweb/pages/payment_methods/header.dart';
import 'package:pweb/pages/payment_methods/method_selector.dart';
import 'package:pweb/pages/payment_methods/send_button.dart';
@@ -13,15 +15,20 @@ import 'package:pweb/pages/dashboard/payouts/payment_form.dart';
import 'package:pweb/pages/payment_methods/widgets/payment_info_section.dart';
import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/providers/page_selector.dart';
import 'package:pweb/providers/payment_flow_provider.dart';
import 'package:pweb/providers/payment_flow.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageBody extends StatelessWidget {
class PaymentPageContent extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final RecipientsProvider recipientProvider;
final PaymentMethodsProvider methodsProvider;
final MethodMap availablePaymentTypes;
final PayoutDestination fallbackDestination;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
@@ -29,9 +36,14 @@ class PaymentPageBody extends StatelessWidget {
final VoidCallback onRecipientCleared;
final VoidCallback onSend;
const PaymentPageBody({
const PaymentPageContent({
super.key,
required this.onBack,
required this.recipient,
required this.recipientProvider,
required this.methodsProvider,
required this.availablePaymentTypes,
required this.fallbackDestination,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
@@ -43,21 +55,9 @@ class PaymentPageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
final dimensions = AppDimensions();
final pageSelector = context.watch<PageSelectorProvider>();
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipientProvider = context.watch<RecipientsProvider>();
final flowProvider = context.watch<PaymentFlowProvider>();
final recipient = pageSelector.selectedRecipient;
final loc = AppLocalizations.of(context)!;
if (methodsProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (methodsProvider.error != null) {
return Center(child: Text(loc.notificationError(methodsProvider.error ?? loc.noErrorInformation)));
}
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
@@ -73,18 +73,20 @@ class PaymentPageBody extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PaymentBackButton(onBack: onBack, pageSelector: pageSelector),
PaymentBackButton(
onBack: onBack,
recipient: recipient,
fallbackDestination: fallbackDestination,
),
SizedBox(height: dimensions.paddingSmall),
PaymentHeader(),
SizedBox(height: dimensions.paddingXXLarge),
SectionTitle(loc.sourceOfFunds),
SizedBox(height: dimensions.paddingSmall),
PaymentMethodSelector(
onMethodChanged: (m) => methodsProvider.setCurrentObject(m.id),
),
SizedBox(height: dimensions.paddingXLarge),
RecipientSection(
recipient: recipient,
dimensions: dimensions,
@@ -95,19 +97,15 @@ class PaymentPageBody extends StatelessWidget {
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
),
SizedBox(height: dimensions.paddingXLarge),
PaymentInfoSection(
dimensions: dimensions,
pageSelector: pageSelector,
flowProvider: flowProvider,
recipient: recipient,
availableTypes: availablePaymentTypes,
),
SizedBox(height: dimensions.paddingLarge),
const PaymentFormWidget(),
SizedBox(height: dimensions.paddingXXXLarge),
SendButton(onPressed: onSend),
SizedBox(height: dimensions.paddingLarge),
@@ -120,31 +118,3 @@ class PaymentPageBody extends StatelessWidget {
);
}
}
class PaymentBackButton extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final PageSelectorProvider pageSelector;
const PaymentBackButton({
super.key,
required this.onBack,
required this.pageSelector,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topLeft,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (onBack != null) {
onBack!(pageSelector.selectedRecipient);
} else {
pageSelector.goBackFromPayment(context);
}
},
),
);
}
}

View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payment_methods/back_button.dart';
import 'package:pweb/pages/payment_methods/header.dart';
import 'package:pweb/pages/payment_methods/method_selector.dart';
import 'package:pweb/pages/payment_methods/send_button.dart';
import 'package:pweb/pages/dashboard/payouts/payment_form.dart';
import 'package:pweb/pages/payment_methods/widgets/payment_info_section.dart';
import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/providers/payment_flow.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageContent extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final RecipientsProvider recipientProvider;
final PaymentMethodsProvider methodsProvider;
final MethodMap availablePaymentTypes;
final PayoutDestination fallbackDestination;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
final VoidCallback onSend;
const PaymentPageContent({
super.key,
required this.onBack,
required this.recipient,
required this.recipientProvider,
required this.methodsProvider,
required this.availablePaymentTypes,
required this.fallbackDestination,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
required this.onRecipientSelected,
required this.onRecipientCleared,
required this.onSend,
});
@override
Widget build(BuildContext context) {
final dimensions = AppDimensions();
final flowProvider = context.watch<PaymentFlowProvider>();
final loc = AppLocalizations.of(context)!;
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth),
child: Material(
elevation: dimensions.elevationSmall,
borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium),
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: EdgeInsets.all(dimensions.paddingLarge),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PaymentBackButton(
onBack: onBack,
recipient: recipient,
fallbackDestination: fallbackDestination,
),
SizedBox(height: dimensions.paddingSmall),
PaymentHeader(),
SizedBox(height: dimensions.paddingXXLarge),
SectionTitle(loc.sourceOfFunds),
SizedBox(height: dimensions.paddingSmall),
PaymentMethodSelector(
onMethodChanged: (m) => methodsProvider.setCurrentObject(m.id),
),
SizedBox(height: dimensions.paddingXLarge),
RecipientSection(
recipient: recipient,
dimensions: dimensions,
recipientProvider: recipientProvider,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,
onRecipientSelected: onRecipientSelected,
onRecipientCleared: onRecipientCleared,
),
SizedBox(height: dimensions.paddingXLarge),
PaymentInfoSection(
dimensions: dimensions,
flowProvider: flowProvider,
recipient: recipient,
availableTypes: availablePaymentTypes,
),
SizedBox(height: dimensions.paddingLarge),
const PaymentFormWidget(),
SizedBox(height: dimensions.paddingXXXLarge),
SendButton(onPressed: onSend),
SizedBox(height: dimensions.paddingLarge),
],
),
),
),
),
),
);
}
}

View File

@@ -6,8 +6,7 @@ import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/payment_methods/form.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/providers/page_selector.dart';
import 'package:pweb/providers/payment_flow_provider.dart';
import 'package:pweb/providers/payment_flow.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/utils/payment/selector_type.dart';
@@ -16,14 +15,14 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentInfoSection extends StatelessWidget {
final AppDimensions dimensions;
final PageSelectorProvider pageSelector;
final MethodMap availableTypes;
final PaymentFlowProvider flowProvider;
final Recipient? recipient;
const PaymentInfoSection({
super.key,
required this.dimensions,
required this.pageSelector,
required this.availableTypes,
required this.flowProvider,
required this.recipient,
});
@@ -32,11 +31,11 @@ class PaymentInfoSection extends StatelessWidget {
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final hasRecipient = recipient != null;
final MethodMap availableTypes = hasRecipient
? pageSelector.getAvailablePaymentTypes()
final MethodMap resolvedAvailableTypes = hasRecipient
? availableTypes
: {for (final type in PaymentType.values) type: null};
if (hasRecipient && availableTypes.isEmpty) {
if (hasRecipient && resolvedAvailableTypes.isEmpty) {
return Text(loc.recipientNoPaymentDetails);
}
@@ -48,7 +47,7 @@ class PaymentInfoSection extends StatelessWidget {
SectionTitle(loc.paymentInfo),
SizedBox(height: dimensions.paddingSmall),
PaymentTypeSelector(
availableTypes: availableTypes,
availableTypes: resolvedAvailableTypes,
selectedType: selectedType,
onSelected: (type) => flowProvider.selectType(
type,
@@ -63,7 +62,7 @@ class PaymentInfoSection extends StatelessWidget {
flowProvider.setManualPaymentData(data);
}
},
initialData: hasRecipient ? availableTypes[selectedType] : flowProvider.manualPaymentData,
initialData: hasRecipient ? resolvedAvailableTypes[selectedType] : flowProvider.manualPaymentData,
isEditable: !hasRecipient,
),
],

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));
}
}