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

@@ -0,0 +1,20 @@
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

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pweb/pages/payment_methods/manage/method_tile.dart';
import 'package:pweb/controllers/payments/payment_config.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

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/payment_methods/manage/advanced.dart';
import 'package:pweb/controllers/payments/payment_config.dart';
import 'package:pweb/pages/payment_methods/manage/header.dart';
import 'package:pweb/pages/payment_methods/manage/list.dart';
class MethodsWidget extends StatefulWidget {
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

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

View File

@@ -1,41 +0,0 @@
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

@@ -1,89 +0,0 @@
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/payment_methods/widgets/state_view.dart';
import 'package:pweb/pages/payment_methods/payment_page/page.dart';
import 'package:pweb/models/control_state.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageBody extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final Recipient? previousRecipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
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;
const PaymentPageBody({
super.key,
required this.onBack,
required this.recipient,
required this.previousRecipient,
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,
});
@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,
previousRecipient: previousRecipient,
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,
);
}
}

View File

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

View File

@@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentHeader extends StatelessWidget {
const PaymentHeader({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dimensions = AppDimensions();
return Row(
children: [
Icon(
Icons.send_rounded,
color: theme.colorScheme.primary,
size: dimensions.iconSizeLarge
),
SizedBox(width: dimensions.spacingSmall),
Text(
AppLocalizations.of(context)!.sendTo,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold
),
),
],
);
}
}

View File

@@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package: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

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

View File

@@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/models/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

@@ -1,67 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/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

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

View File

@@ -1,95 +0,0 @@
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/payment_methods/widgets/card.dart';
import 'package:pweb/pages/payment_methods/widgets/search.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class RecipientSection extends StatelessWidget {
final Recipient? recipient;
final Recipient? previousRecipient;
final AppDimensions dimensions;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onRecipientCleared;
const RecipientSection({
super.key,
required this.recipient,
required this.previousRecipient,
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,
});
@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;
final prev = previousRecipient;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(loc.recipient),
SizedBox(height: dimensions.paddingSmall),
RecipientSearchField(
controller: searchController,
onChanged: onSearchChanged,
focusNode: searchFocusNode,
),
if (prev != null) ...[
SizedBox(height: dimensions.paddingSmall),
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.undo),
title: Text(loc.back),
subtitle: Text(prev.name),
onTap: () => onRecipientSelected(prev),
),
],
if (hasQuery) ...[
SizedBox(height: dimensions.paddingMedium),
RecipientSearchResults(
dimensions: dimensions,
recipientProvider: recipientProvider,
results: filteredRecipients,
onRecipientSelected: onRecipientSelected,
),
],
],
);
},
);
}
}

View File

@@ -1,71 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,21 +0,0 @@
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));
}
}