Multiple Wallet support, history of each wallet and updated payment page

This commit is contained in:
Arseni
2025-11-21 19:22:23 +03:00
parent 4c64a8d6e6
commit 87636a7ec3
68 changed files with 2154 additions and 701 deletions

View File

@@ -1,232 +1,120 @@
import 'package:amplitude_flutter/amplitude.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/dashboard/payouts/payment_form.dart';
import 'package:pweb/pages/dashboard/payouts/single/form/details.dart';
import 'package:pweb/pages/dashboard/payouts/single/form/header.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/providers/payment_methods.dart';
import 'package:pweb/providers/recipient.dart';
import 'package:pweb/services/amplitude.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/utils/payment/dropdown.dart';
import 'package:pweb/utils/payment/selector_type.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
//TODO: decide whether to make AppDimensions universal for the whole app or leave it as it is - unique for this page alone
class PaymentPage extends StatefulWidget {
final PaymentType? type;
final ValueChanged<Recipient?>? onBack;
const PaymentPage({super.key, this.type, this.onBack});
const PaymentPage({super.key, this.onBack});
@override
State<PaymentPage> createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> {
late Map<PaymentType, Object> _availableTypes;
late PaymentType _selectedType;
bool _isFormVisible = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final recipientProvider = context.watch<RecipientProvider>();
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipient = recipientProvider.selectedRecipient;
// Initialize available types based on whether we have a recipient
if (recipient != null) {
// We have a recipient - use their payment methods
_availableTypes = {
if (recipient.card != null) PaymentType.card: recipient.card!,
if (recipient.iban != null) PaymentType.iban: recipient.iban!,
if (recipient.wallet != null) PaymentType.wallet: recipient.wallet!,
if (recipient.bank != null) PaymentType.bankAccount: recipient.bank!,
};
// Set selected type if it's available, otherwise use first available type
if (_availableTypes.containsKey(_selectedType)) {
// Keep current selection if valid
} else if (_availableTypes.isNotEmpty) {
_selectedType = _availableTypes.keys.first;
} else {
// Fallback if recipient has no payment methods
_selectedType = PaymentType.bankAccount;
}
} else {
// No recipient - we're creating a new payment from scratch
_availableTypes = {};
_selectedType = widget.type ?? PaymentType.bankAccount;
_isFormVisible = true; // Always show form when creating new payment
}
// Load payment methods if not already loaded
if (methodsProvider.methods.isEmpty && !methodsProvider.isLoading) {
WidgetsBinding.instance.addPostFrameCallback((_) {
methodsProvider.loadMethods();
});
}
}
late final TextEditingController _searchController;
late final FocusNode _searchFocusNode;
late final PaymentFlowProvider _flowProvider;
@override
void initState() {
super.initState();
// Initial values
_availableTypes = {};
_selectedType = widget.type ?? PaymentType.bankAccount;
_searchController = TextEditingController();
_searchFocusNode = FocusNode();
final pageSelector = context.read<PageSelectorProvider>();
_flowProvider = PaymentFlowProvider(
initialType: pageSelector.getDefaultPaymentType(),
);
WidgetsBinding.instance.addPostFrameCallback((_) => _initializePaymentPage());
}
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
_flowProvider.dispose();
super.dispose();
}
void _initializePaymentPage() {
final pageSelector = context.read<PageSelectorProvider>();
final methodsProvider = context.read<PaymentMethodsProvider>();
final recipientProvider = context.read<RecipientProvider>();
pageSelector.handleWalletAutoSelection();
if (methodsProvider.methods.isEmpty && !methodsProvider.isLoading) {
methodsProvider.loadMethods();
}
if (recipientProvider.recipients.isEmpty && !recipientProvider.isLoading) {
recipientProvider.loadRecipients();
}
_flowProvider.syncWithSelector(pageSelector);
}
void _handleSearchChanged(String query) {
context.read<RecipientProvider>().setQuery(query);
}
void _handleRecipientSelected(Recipient recipient) {
final pageSelector = context.read<PageSelectorProvider>();
final recipientProvider = context.read<RecipientProvider>();
recipientProvider.selectRecipient(recipient);
pageSelector.selectRecipient(recipient);
_flowProvider.reset(pageSelector);
_clearSearchField();
}
void _handleRecipientCleared() {
final pageSelector = context.read<PageSelectorProvider>();
final recipientProvider = context.read<RecipientProvider>();
recipientProvider.selectRecipient(null);
pageSelector.selectRecipient(null);
_flowProvider.reset(pageSelector);
_clearSearchField();
}
void _clearSearchField() {
_searchController.clear();
_searchFocusNode.unfocus();
context.read<RecipientProvider>().setQuery('');
}
void _handleSendPayment() {
// TODO: Handle Payment logic
// AmplitudeService.paymentInitiated();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dimensions = AppDimensions();
final recipientProvider = context.watch<RecipientProvider>();
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipient = recipientProvider.selectedRecipient;
final pageSelector = context.watch<PageSelectorProvider>();
_flowProvider.syncWithSelector(pageSelector);
// Show loading state for payment methods
if (methodsProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
// Show error state for payment methods
if (methodsProvider.error != null) {
return Center(
child: Text('Error: ${methodsProvider.error}'),
);
}
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth),
child: Material(
elevation: dimensions.elevationSmall,
borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium),
color: theme.colorScheme.onSecondary,
child: Padding(
padding: EdgeInsets.all(dimensions.paddingLarge),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back button
Align(
alignment: Alignment.topLeft,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
widget.onBack?.call(recipient);
},
),
),
SizedBox(height: dimensions.paddingSmall),
// Header
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
),
),
],
),
SizedBox(height: dimensions.paddingXXLarge),
// Payment method dropdown (user's payment methods)
PaymentMethodDropdown(
methods: methodsProvider.methods,
initialValue: methodsProvider.selectedMethod,
onChanged: (method) {
methodsProvider.selectMethod(method);
},
),
SizedBox(height: dimensions.paddingXLarge),
// Recipient section (only show if we have a recipient)
if (recipient != null) ...[
RecipientHeader(recipient: recipient),
SizedBox(height: dimensions.paddingMedium),
// Payment type selector (recipient's payment methods)
if (_availableTypes.isNotEmpty)
PaymentTypeSelector(
availableTypes: _availableTypes,
selectedType: _selectedType,
onSelected: (type) => setState(() => _selectedType = type),
),
SizedBox(height: dimensions.paddingMedium),
],
// Payment details section
PaymentDetailsSection(
isFormVisible: recipient == null || _isFormVisible,
onToggle: recipient != null
? () => setState(() => _isFormVisible = !_isFormVisible)
: null, // No toggle when creating new payment
selectedType: _selectedType,
data: _availableTypes[_selectedType],
isEditable: recipient == null,
),
const PaymentFormWidget(),
SizedBox(height: dimensions.paddingXXXLarge),
Center(
child: SizedBox(
width: dimensions.buttonWidth,
height: dimensions.buttonHeight,
child: InkWell(
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
onTap: () =>
// TODO: Handle Payment logic
AmplitudeService.pageOpened(PayoutDestination.payment), //TODO: replace with payment event
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
),
child: Center(
child: Text(
AppLocalizations.of(context)!.send,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSecondary,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
),
SizedBox(height: dimensions.paddingLarge),
],
),
),
),
),
return ChangeNotifierProvider.value(
value: _flowProvider,
child: PaymentPageBody(
onBack: widget.onBack,
searchController: _searchController,
searchFocusNode: _searchFocusNode,
onSearchChanged: _handleSearchChanged,
onRecipientSelected: _handleRecipientSelected,
onRecipientCleared: _handleRecipientCleared,
onSend: _handleSendPayment,
),
);
}
}
}