From edfdef5211a71ad2b7705849601670cd79498242 Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 26 Dec 2025 11:19:49 +0300 Subject: [PATCH 1/5] Fixed search field in payment page and cleaned up payment flow --- .../lib/api/responses/payment/payment.dart | 20 --- .../pshared/lib/provider/payment/flow.dart | 111 +++++++++------ .../lib/provider/payment/provider.dart | 64 --------- .../lib/provider/payment/quotation.dart | 11 +- .../lib/provider/recipient/pmethods.dart | 20 +++ .../lib/provider/recipient/provider.dart | 23 +++ .../pshared/lib/service/payment/service.dart | 36 ----- .../lib/pages/dashboard/payouts/widget.dart | 30 ++++ .../pweb/lib/pages/payment_methods/page.dart | 134 +++++++----------- .../payment_methods/payment_page/body.dart | 4 - .../payment_methods/payment_page/content.dart | 14 +- .../payment_methods/payment_page/page.dart | 18 +-- .../widgets/payment_info_section.dart | 19 +-- .../widgets/recipient_section.dart | 59 +++++--- 14 files changed, 246 insertions(+), 317 deletions(-) delete mode 100644 frontend/pshared/lib/api/responses/payment/payment.dart delete mode 100644 frontend/pshared/lib/provider/payment/provider.dart delete mode 100644 frontend/pshared/lib/service/payment/service.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/widget.dart diff --git a/frontend/pshared/lib/api/responses/payment/payment.dart b/frontend/pshared/lib/api/responses/payment/payment.dart deleted file mode 100644 index b5b5d3e..0000000 --- a/frontend/pshared/lib/api/responses/payment/payment.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -import 'package:pshared/api/responses/base.dart'; -import 'package:pshared/api/responses/token.dart'; -import 'package:pshared/data/dto/payment/payment.dart'; - -part 'payment.g.dart'; - - -@JsonSerializable(explicitToJson: true) -class PaymentResponse extends BaseAuthorizedResponse { - - final PaymentDTO payment; - - const PaymentResponse({required super.accessToken, required this.payment}); - - factory PaymentResponse.fromJson(Map json) => _$PaymentResponseFromJson(json); - @override - Map toJson() => _$PaymentResponseToJson(this); -} diff --git a/frontend/pshared/lib/provider/payment/flow.dart b/frontend/pshared/lib/provider/payment/flow.dart index e4f3f73..12b909f 100644 --- a/frontend/pshared/lib/provider/payment/flow.dart +++ b/frontend/pshared/lib/provider/payment/flow.dart @@ -3,11 +3,14 @@ import 'package:flutter/foundation.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/recipient/pmethods.dart'; class PaymentFlowProvider extends ChangeNotifier { PaymentType _selectedType; PaymentMethodData? _manualPaymentData; + MethodMap _availableTypes = {}; + Recipient? _recipient; PaymentFlowProvider({ required PaymentType initialType, @@ -15,57 +18,40 @@ class PaymentFlowProvider extends ChangeNotifier { PaymentType get selectedType => _selectedType; PaymentMethodData? get manualPaymentData => _manualPaymentData; + Recipient? get recipient => _recipient; - void sync({ + bool get hasRecipient => _recipient != null; + + MethodMap get availableTypes => hasRecipient + ? _availableTypes + : {for (final type in PaymentType.values) type: null}; + + PaymentMethodData? get selectedPaymentData => + hasRecipient ? _availableTypes[_selectedType] : _manualPaymentData; + + void syncWith({ required Recipient? recipient, - required MethodMap availableTypes, + required PaymentMethodsProvider methodsProvider, PaymentType? preferredType, - }) { - final resolvedType = _resolveSelectedType( - recipient: recipient, - availableTypes: availableTypes, - preferredType: preferredType, - ); - - var hasChanges = false; - if (resolvedType != _selectedType) { - _selectedType = resolvedType; - hasChanges = true; - } - - if (recipient != null && _manualPaymentData != null) { - _manualPaymentData = null; - hasChanges = true; - } - - if (hasChanges) notifyListeners(); - } + }) => + _applyState( + recipient: recipient, + availableTypes: methodsProvider.availableTypesForRecipient(recipient), + preferredType: preferredType, + forceResetManualData: false, + ); void reset({ required Recipient? recipient, - required MethodMap availableTypes, + required PaymentMethodsProvider methodsProvider, PaymentType? preferredType, - }) { - final resolvedType = _resolveSelectedType( - recipient: recipient, - availableTypes: availableTypes, - preferredType: preferredType, - ); - - var hasChanges = false; - - if (resolvedType != _selectedType) { - _selectedType = resolvedType; - hasChanges = true; - } - - if (_manualPaymentData != null) { - _manualPaymentData = null; - hasChanges = true; - } - - if (hasChanges) notifyListeners(); - } + }) => + _applyState( + recipient: recipient, + availableTypes: methodsProvider.availableTypesForRecipient(recipient), + preferredType: preferredType, + forceResetManualData: true, + ); void selectType(PaymentType type, {bool resetManualData = false}) { if (_selectedType == type && (!resetManualData || _manualPaymentData == null)) { @@ -107,4 +93,41 @@ class PaymentFlowProvider extends ChangeNotifier { return availableTypes.keys.first; } + + void _applyState({ + required Recipient? recipient, + required MethodMap availableTypes, + required PaymentType? preferredType, + required bool forceResetManualData, + }) { + final resolvedType = _resolveSelectedType( + recipient: recipient, + availableTypes: availableTypes, + preferredType: preferredType, + ); + + var hasChanges = false; + + if (_recipient != recipient) { + _recipient = recipient; + hasChanges = true; + } + + if (!mapEquals(_availableTypes, availableTypes)) { + _availableTypes = availableTypes; + hasChanges = true; + } + + if (resolvedType != _selectedType) { + _selectedType = resolvedType; + hasChanges = true; + } + + if ((recipient != null || forceResetManualData) && _manualPaymentData != null) { + _manualPaymentData = null; + hasChanges = true; + } + + if (hasChanges) notifyListeners(); + } } diff --git a/frontend/pshared/lib/provider/payment/provider.dart b/frontend/pshared/lib/provider/payment/provider.dart deleted file mode 100644 index 6afd032..0000000 --- a/frontend/pshared/lib/provider/payment/provider.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/foundation.dart'; - -import 'package:pshared/models/payment/payment.dart'; -import 'package:pshared/provider/organizations.dart'; -import 'package:pshared/provider/payment/quotation.dart'; -import 'package:pshared/provider/resource.dart'; -import 'package:pshared/service/payment/service.dart'; - - -class PaymentProvider extends ChangeNotifier { - late OrganizationsProvider _organization; - late QuotationProvider _quotation; - - Resource _payment = Resource(data: null, isLoading: false, error: null); - bool _isLoaded = false; - - void update(OrganizationsProvider organization, QuotationProvider quotation) { - _quotation = quotation; - _organization = organization; - } - - Payment? get payment => _payment.data; - bool get isLoading => _payment.isLoading; - Exception? get error => _payment.error; - bool get isReady => _isLoaded && !_payment.isLoading && _payment.error == null; - - void _setResource(Resource payment) { - _payment = payment; - notifyListeners(); - } - - Future pay({String? idempotencyKey, Map? metadata}) async { - if (!_organization.isOrganizationSet) throw StateError('Organization is not set'); - if (!_quotation.isReady) throw StateError('Quotation is not ready'); - final quoteRef = _quotation.quotation?.quoteRef; - if (quoteRef == null || quoteRef.isEmpty) { - throw StateError('Quotation reference is not set'); - } - - _setResource(_payment.copyWith(isLoading: true, error: null)); - try { - final response = await PaymentService.pay( - _organization.current.id, - quoteRef, - idempotencyKey: idempotencyKey, - metadata: metadata, - ); - _isLoaded = true; - _setResource(_payment.copyWith(data: response, isLoading: false, error: null)); - } catch (e) { - _setResource(_payment.copyWith( - data: null, - error: e is Exception ? e : Exception(e.toString()), - isLoading: false, - )); - } - return _payment.data; - } - - void reset() { - _setResource(Resource(data: null, isLoading: false, error: null)); - _isLoaded = false; - } -} diff --git a/frontend/pshared/lib/provider/payment/quotation.dart b/frontend/pshared/lib/provider/payment/quotation.dart index a36bacc..d093f49 100644 --- a/frontend/pshared/lib/provider/payment/quotation.dart +++ b/frontend/pshared/lib/provider/payment/quotation.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; - import 'package:uuid/uuid.dart'; import 'package:pshared/api/requests/payment/quote.dart'; @@ -20,7 +18,6 @@ import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/payment/amount.dart'; import 'package:pshared/provider/payment/flow.dart'; import 'package:pshared/provider/payment/wallets.dart'; -import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/payment/quotation.dart'; import 'package:pshared/utils/currency.dart'; @@ -36,12 +33,10 @@ class QuotationProvider extends ChangeNotifier { PaymentAmountProvider payment, WalletsProvider wallets, PaymentFlowProvider flow, - PaymentMethodsProvider methods, ) { _organizations = venue; - final t = flow.selectedType; - final method = methods.methods.firstWhereOrNull((m) => m.type == t); - if ((wallets.selectedWallet != null) && (method != null)) { + final destination = flow.selectedPaymentData; + if ((wallets.selectedWallet != null) && (destination != null)) { getQuotation(PaymentIntent( kind: PaymentKind.payout, amount: Money( @@ -49,7 +44,7 @@ class QuotationProvider extends ChangeNotifier { // TODO: adapt to possible other sources currency: currencyCodeToString(wallets.selectedWallet!.currency), ), - destination: method.data, + destination: destination, source: ManagedWalletPaymentMethod( managedWalletRef: wallets.selectedWallet!.id, ), diff --git a/frontend/pshared/lib/provider/recipient/pmethods.dart b/frontend/pshared/lib/provider/recipient/pmethods.dart index a4ecc87..9af60e4 100644 --- a/frontend/pshared/lib/provider/recipient/pmethods.dart +++ b/frontend/pshared/lib/provider/recipient/pmethods.dart @@ -5,6 +5,8 @@ import 'package:pshared/models/describable.dart'; import 'package:pshared/models/organization/bound.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/models/permissions/bound.dart'; import 'package:pshared/models/storable.dart'; import 'package:pshared/provider/organizations.dart'; @@ -20,6 +22,24 @@ class PaymentMethodsProvider extends GenericProvider { List get methods => List.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt))); + List methodsForRecipient(Recipient? recipient) { + if (recipient == null || !isReady) return []; + + return methods + .where((method) => !method.isArchived && method.recipientRef == recipient.id) + .toList(); + } + + MethodMap availableTypesForRecipient(Recipient? recipient) => { + for (final method in methodsForRecipient(recipient)) method.type: method.data, + }; + + PaymentMethod? findMethodByType({ + required PaymentType type, + required Recipient? recipient, + }) => + methodsForRecipient(recipient).firstWhereOrNull((method) => method.type == type); + void updateProviders(OrganizationsProvider organizations, RecipientsProvider recipients) { if (recipients.currentObject != null) loadMethods(organizations, recipients.currentObject?.id); } diff --git a/frontend/pshared/lib/provider/recipient/provider.dart b/frontend/pshared/lib/provider/recipient/provider.dart index 756e348..f4f35e6 100644 --- a/frontend/pshared/lib/provider/recipient/provider.dart +++ b/frontend/pshared/lib/provider/recipient/provider.dart @@ -14,6 +14,7 @@ class RecipientsProvider extends GenericProvider { RecipientFilter _selectedFilter = RecipientFilter.all; String _query = ''; + String? _previousRecipientRef; RecipientFilter get selectedFilter => _selectedFilter; String get query => _query; @@ -22,6 +23,10 @@ class RecipientsProvider extends GenericProvider { RecipientsProvider() : super(service: RecipientService.basicService); + Recipient? get previousRecipient => _previousRecipientRef == null + ? null + : getItemByRef(_previousRecipientRef!); + List get filteredRecipients { List filtered = recipients.where((r) { switch (_selectedFilter) { @@ -53,6 +58,24 @@ class RecipientsProvider extends GenericProvider { notifyListeners(); } + @override + bool setCurrentObject(String? objectRef) { + final currentRef = currentObject?.id; + final didUpdate = super.setCurrentObject(objectRef); + + if (didUpdate && currentRef != null && currentRef != objectRef) { + _previousRecipientRef = currentRef; + } + + return didUpdate; + } + + void restorePreviousRecipient() { + if (_previousRecipientRef != null) { + setCurrentObject(_previousRecipientRef); + } + } + Future create({ required String name, required String email, diff --git a/frontend/pshared/lib/service/payment/service.dart b/frontend/pshared/lib/service/payment/service.dart deleted file mode 100644 index b8e0a27..0000000 --- a/frontend/pshared/lib/service/payment/service.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:logging/logging.dart'; - -import 'package:uuid/uuid.dart'; - -import 'package:pshared/api/requests/payment/initiate.dart'; -import 'package:pshared/api/responses/payment/payment.dart'; -import 'package:pshared/data/mapper/payment/payment_response.dart'; -import 'package:pshared/models/payment/payment.dart'; -import 'package:pshared/service/authorization/service.dart'; -import 'package:pshared/service/services.dart'; - - -class PaymentService { - static final _logger = Logger('service.payment'); - static const String _objectType = Services.payments; - - static Future pay( - String organizationRef, - String quotationRef, { - String? idempotencyKey, - Map? metadata, - }) async { - _logger.fine('Executing payment for quotation $quotationRef in $organizationRef'); - final request = InitiatePaymentRequest( - idempotencyKey: idempotencyKey ?? Uuid().v4(), - quoteRef: quotationRef, - metadata: metadata, - ); - final response = await AuthorizationService.getPOSTResponse( - _objectType, - '/by-quote/$organizationRef', - request.toJson(), - ); - return PaymentResponse.fromJson(response).payment.toDomain(); - } -} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/widget.dart new file mode 100644 index 0000000..a88849e --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/widget.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/organizations.dart'; +import 'package:pshared/provider/payment/amount.dart'; +import 'package:pshared/provider/payment/flow.dart'; +import 'package:pshared/provider/payment/quotation.dart'; +import 'package:pshared/provider/payment/wallets.dart'; + +import 'package:pweb/pages/dashboard/payouts/form.dart'; + + +class PaymentFromWrappingWidget extends StatelessWidget { + const PaymentFromWrappingWidget({super.key}); + + @override + Widget build(BuildContext context) => MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => PaymentAmountProvider(), + ), + ChangeNotifierProxyProvider4( + create: (_) => QuotationProvider(), + update: (context, orgnization, payment, wallet, flow, provider) => provider!..update(orgnization, payment, wallet, flow), + ), + ], + child: const PaymentFormWidget(), + ); +} diff --git a/frontend/pweb/lib/pages/payment_methods/page.dart b/frontend/pweb/lib/pages/payment_methods/page.dart index 2c10623..870c02d 100644 --- a/frontend/pweb/lib/pages/payment_methods/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/page.dart @@ -4,21 +4,17 @@ 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/organizations.dart'; -import 'package:pshared/provider/payment/amount.dart'; import 'package:pshared/provider/payment/flow.dart'; -import 'package:pshared/provider/payment/provider.dart'; -import 'package:pshared/provider/payment/quotation.dart'; import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/recipient/provider.dart'; - import 'package:pshared/models/payment/wallet.dart'; -import 'package:pweb/pages/payment_methods/payment_page/body.dart'; import 'package:pshared/provider/payment/wallets.dart'; + + +import 'package:pweb/pages/payment_methods/payment_page/body.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/services/posthog.dart'; @@ -42,12 +38,16 @@ class PaymentPage extends StatefulWidget { class _PaymentPageState extends State { late final TextEditingController _searchController; late final FocusNode _searchFocusNode; + late final PaymentFlowProvider _flowProvider; @override void initState() { super.initState(); _searchController = TextEditingController(); _searchFocusNode = FocusNode(); + _flowProvider = PaymentFlowProvider( + initialType: widget.initialPaymentType ?? PaymentType.bankAccount, + ); WidgetsBinding.instance.addPostFrameCallback((_) => _initializePaymentPage()); } @@ -56,32 +56,47 @@ class _PaymentPageState extends State { void dispose() { _searchController.dispose(); _searchFocusNode.dispose(); + _flowProvider.dispose(); super.dispose(); } void _initializePaymentPage() { final methodsProvider = context.read(); _handleWalletAutoSelection(methodsProvider); + + final recipient = context.read().currentObject; + _flowProvider.syncWith( + recipient: recipient, + methodsProvider: methodsProvider, + preferredType: widget.initialPaymentType, + ); } void _handleSearchChanged(String query) { context.read().setQuery(query); } - void _handleRecipientSelected(BuildContext context, Recipient recipient) { + void _handleRecipientSelected(Recipient recipient) { final recipientProvider = context.read(); + final methodsProvider = context.read(); + recipientProvider.setCurrentObject(recipient.id); + _flowProvider.reset( + recipient: recipient, + methodsProvider: methodsProvider, + preferredType: widget.initialPaymentType, + ); _clearSearchField(); } - void _handleRecipientCleared(BuildContext context) { + void _handleRecipientCleared() { final recipientProvider = context.read(); final methodsProvider = context.read(); recipientProvider.setCurrentObject(null); - context.read().reset( + _flowProvider.reset( recipient: null, - availableTypes: _availablePaymentTypes(null, methodsProvider), + methodsProvider: methodsProvider, preferredType: widget.initialPaymentType, ); _clearSearchField(); @@ -93,65 +108,39 @@ class _PaymentPageState extends State { context.read().setQuery(''); } - void _handleSendPayment(BuildContext context) { - if (context.read().isReady) { - context.read().pay(); - PosthogService.paymentInitiated( - method: context.read().selectedType, - ); - } + void _handleSendPayment() { + // TODO: Handle Payment logic + PosthogService.paymentInitiated(method: _flowProvider.selectedType); } @override Widget build(BuildContext context) { final methodsProvider = context.watch(); - final recipientProvider = context.watch(); - final recipient = recipientProvider.currentObject; - final availableTypes = _availablePaymentTypes(recipient, methodsProvider); + final recipientProvider = context.read(); + final recipient = context.select( + (provider) => provider.currentObject, + ); - return MultiProvider( - providers: [ - ChangeNotifierProxyProvider2( - create: (_) => PaymentFlowProvider( - initialType: widget.initialPaymentType ?? PaymentType.bankAccount, - ), - update: (_, recipients, methods, flow) { - final currentRecipient = recipients.currentObject; - flow!.sync( - recipient: currentRecipient, - availableTypes: _availablePaymentTypes(currentRecipient, methods), - preferredType: currentRecipient != null ? widget.initialPaymentType : null, - ); - return flow; - }, - ), - ChangeNotifierProvider( - create: (_) => PaymentAmountProvider(), - ), - ChangeNotifierProxyProvider5( - create: (_) => QuotationProvider(), - update: (_, organization, payment, wallet, flow, methods, provider) => provider!..update(organization, payment, wallet, flow, methods), - ), - ChangeNotifierProxyProvider2( - create: (_) => PaymentProvider(), - update: (_, organization, quotation, provider) => provider!..update(organization, quotation), - ), - ], - child: Builder( - builder: (innerContext) => PaymentPageBody( - onBack: widget.onBack, - fallbackDestination: widget.fallbackDestination, - recipient: recipient, - recipientProvider: recipientProvider, - methodsProvider: methodsProvider, - availablePaymentTypes: availableTypes, - searchController: _searchController, - searchFocusNode: _searchFocusNode, - onSearchChanged: _handleSearchChanged, - onRecipientSelected: (selected) => _handleRecipientSelected(innerContext, selected), - onRecipientCleared: () => _handleRecipientCleared(innerContext), - onSend: () => _handleSendPayment(innerContext), - ), + _flowProvider.syncWith( + 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, + searchController: _searchController, + searchFocusNode: _searchFocusNode, + onSearchChanged: _handleSearchChanged, + onRecipientSelected: _handleRecipientSelected, + onRecipientCleared: _handleRecipientCleared, + onSend: _handleSendPayment, ), ); } @@ -166,21 +155,6 @@ class _PaymentPageState extends State { } } - 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, @@ -195,4 +169,4 @@ class _PaymentPageState extends State { (method.description?.contains(wallet.walletUserID) ?? false), ); } -} +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/body.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/body.dart index 2caf9da..6d58a6a 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/body.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/body.dart @@ -1,6 +1,5 @@ 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'; @@ -17,7 +16,6 @@ class PaymentPageBody extends StatelessWidget { final Recipient? recipient; final RecipientsProvider recipientProvider; final PaymentMethodsProvider methodsProvider; - final MethodMap availablePaymentTypes; final PayoutDestination fallbackDestination; final TextEditingController searchController; final FocusNode searchFocusNode; @@ -32,7 +30,6 @@ class PaymentPageBody extends StatelessWidget { required this.recipient, required this.recipientProvider, required this.methodsProvider, - required this.availablePaymentTypes, required this.fallbackDestination, required this.searchController, required this.searchFocusNode, @@ -61,7 +58,6 @@ class PaymentPageBody extends StatelessWidget { recipient: recipient, recipientProvider: recipientProvider, methodsProvider: methodsProvider, - availablePaymentTypes: availablePaymentTypes, fallbackDestination: fallbackDestination, searchController: searchController, searchFocusNode: searchFocusNode, diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/content.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/content.dart index 2638330..ebb9f3e 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/content.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/content.dart @@ -1,12 +1,8 @@ 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:pshared/provider/payment/flow.dart'; import 'package:pweb/pages/payment_methods/payment_page/back_button.dart'; import 'package:pweb/pages/payment_methods/payment_page/header.dart'; @@ -27,7 +23,6 @@ class PaymentPageContent extends StatelessWidget { final Recipient? recipient; final RecipientsProvider recipientProvider; final PaymentMethodsProvider methodsProvider; - final MethodMap availablePaymentTypes; final PayoutDestination fallbackDestination; final TextEditingController searchController; final FocusNode searchFocusNode; @@ -42,7 +37,6 @@ class PaymentPageContent extends StatelessWidget { required this.recipient, required this.recipientProvider, required this.methodsProvider, - required this.availablePaymentTypes, required this.fallbackDestination, required this.searchController, required this.searchFocusNode, @@ -55,7 +49,6 @@ class PaymentPageContent extends StatelessWidget { @override Widget build(BuildContext context) { final dimensions = AppDimensions(); - final flowProvider = context.watch(); final loc = AppLocalizations.of(context)!; return Align( @@ -98,12 +91,7 @@ class PaymentPageContent extends StatelessWidget { onRecipientCleared: onRecipientCleared, ), SizedBox(height: dimensions.paddingXLarge), - PaymentInfoSection( - dimensions: dimensions, - flowProvider: flowProvider, - recipient: recipient, - availableTypes: availablePaymentTypes, - ), + PaymentInfoSection(dimensions: dimensions), SizedBox(height: dimensions.paddingLarge), const PaymentFormWidget(), SizedBox(height: dimensions.paddingXXXLarge), diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart index 2352d0d..7d23770 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart @@ -1,14 +1,10 @@ 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:pshared/provider/payment/flow.dart'; -import 'package:pweb/pages/dashboard/payouts/form.dart'; +import 'package:pweb/pages/dashboard/payouts/widget.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'; @@ -27,7 +23,6 @@ class PaymentPageContent extends StatelessWidget { final Recipient? recipient; final RecipientsProvider recipientProvider; final PaymentMethodsProvider methodsProvider; - final MethodMap availablePaymentTypes; final PayoutDestination fallbackDestination; final TextEditingController searchController; final FocusNode searchFocusNode; @@ -42,7 +37,6 @@ class PaymentPageContent extends StatelessWidget { required this.recipient, required this.recipientProvider, required this.methodsProvider, - required this.availablePaymentTypes, required this.fallbackDestination, required this.searchController, required this.searchFocusNode, @@ -55,7 +49,6 @@ class PaymentPageContent extends StatelessWidget { @override Widget build(BuildContext context) { final dimensions = AppDimensions(); - final flowProvider = context.watch(); final loc = AppLocalizations.of(context)!; return Align( @@ -98,14 +91,9 @@ class PaymentPageContent extends StatelessWidget { onRecipientCleared: onRecipientCleared, ), SizedBox(height: dimensions.paddingXLarge), - PaymentInfoSection( - dimensions: dimensions, - flowProvider: flowProvider, - recipient: recipient, - availableTypes: availablePaymentTypes, - ), + PaymentInfoSection(dimensions: dimensions), SizedBox(height: dimensions.paddingLarge), - const PaymentFormWidget(), + const PaymentFromWrappingWidget(), SizedBox(height: dimensions.paddingXXXLarge), SendButton(onPressed: onSend), SizedBox(height: dimensions.paddingLarge), diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart b/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart index 600184e..bb1228e 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + import 'package:pshared/models/payment/methods/data.dart'; -import 'package:pshared/models/payment/type.dart'; -import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/provider/payment/flow.dart'; import 'package:pweb/pages/payment_methods/form.dart'; @@ -15,25 +15,18 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class PaymentInfoSection extends StatelessWidget { final AppDimensions dimensions; - final MethodMap availableTypes; - final PaymentFlowProvider flowProvider; - final Recipient? recipient; const PaymentInfoSection({ super.key, required this.dimensions, - required this.availableTypes, - required this.flowProvider, - required this.recipient, }); @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; - final hasRecipient = recipient != null; - final MethodMap resolvedAvailableTypes = hasRecipient - ? availableTypes - : {for (final type in PaymentType.values) type: null}; + final flowProvider = context.watch(); + final hasRecipient = flowProvider.hasRecipient; + final MethodMap resolvedAvailableTypes = flowProvider.availableTypes; if (hasRecipient && resolvedAvailableTypes.isEmpty) { return Text(loc.recipientNoPaymentDetails); @@ -62,7 +55,7 @@ class PaymentInfoSection extends StatelessWidget { flowProvider.setManualPaymentData(data); } }, - initialData: hasRecipient ? resolvedAvailableTypes[selectedType] : flowProvider.manualPaymentData, + initialData: flowProvider.selectedPaymentData, isEditable: !hasRecipient, ), ], diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart index 8d6e718..c2ad789 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart @@ -45,25 +45,44 @@ class RecipientSection extends StatelessWidget { ); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SectionTitle(loc.recipient), - SizedBox(height: dimensions.paddingSmall), - RecipientSearchField( - controller: searchController, - onChanged: onSearchChanged, - focusNode: searchFocusNode, - ), - if (recipientProvider.query.isNotEmpty) ...[ - SizedBox(height: dimensions.paddingMedium), - RecipientSearchResults( - dimensions: dimensions, - recipientProvider: recipientProvider, - onRecipientSelected: onRecipientSelected, - ), - ], - ], + return AnimatedBuilder( + animation: recipientProvider, + builder: (context, _) { + final previousRecipient = recipientProvider.previousRecipient; + final hasQuery = recipientProvider.query.isNotEmpty; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionTitle(loc.recipient), + SizedBox(height: dimensions.paddingSmall), + RecipientSearchField( + controller: searchController, + onChanged: onSearchChanged, + focusNode: searchFocusNode, + ), + if (previousRecipient != null) ...[ + SizedBox(height: dimensions.paddingSmall), + ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.undo), + title: Text(loc.back), + subtitle: Text(previousRecipient.name), + onTap: () => onRecipientSelected(previousRecipient), + ), + ], + if (hasQuery) ...[ + SizedBox(height: dimensions.paddingMedium), + RecipientSearchResults( + dimensions: dimensions, + recipientProvider: recipientProvider, + onRecipientSelected: onRecipientSelected, + ), + ], + ], + ); + }, ); } -} \ No newline at end of file +} -- 2.49.1 From 1811571f80bb97e01d0b3de73ad515512b42f702 Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 26 Dec 2025 13:29:51 +0300 Subject: [PATCH 2/5] Fixed search field in payment page and cleaned up paymentFlow --- .../lib/api/responses/payment/payment.dart | 20 ++++++ .../lib/provider/payment/provider.dart | 64 +++++++++++++++++ .../pshared/lib/service/payment/service.dart | 35 ++++++++++ frontend/pweb/lib/main.dart | 23 +++++- .../lib/pages/dashboard/payouts/widget.dart | 21 +----- .../pweb/lib/pages/payment_methods/page.dart | 70 +++++++++++-------- 6 files changed, 182 insertions(+), 51 deletions(-) create mode 100644 frontend/pshared/lib/api/responses/payment/payment.dart create mode 100644 frontend/pshared/lib/provider/payment/provider.dart create mode 100644 frontend/pshared/lib/service/payment/service.dart diff --git a/frontend/pshared/lib/api/responses/payment/payment.dart b/frontend/pshared/lib/api/responses/payment/payment.dart new file mode 100644 index 0000000..b5b5d3e --- /dev/null +++ b/frontend/pshared/lib/api/responses/payment/payment.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/base.dart'; +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/data/dto/payment/payment.dart'; + +part 'payment.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class PaymentResponse extends BaseAuthorizedResponse { + + final PaymentDTO payment; + + const PaymentResponse({required super.accessToken, required this.payment}); + + factory PaymentResponse.fromJson(Map json) => _$PaymentResponseFromJson(json); + @override + Map toJson() => _$PaymentResponseToJson(this); +} diff --git a/frontend/pshared/lib/provider/payment/provider.dart b/frontend/pshared/lib/provider/payment/provider.dart new file mode 100644 index 0000000..6afd032 --- /dev/null +++ b/frontend/pshared/lib/provider/payment/provider.dart @@ -0,0 +1,64 @@ +import 'package:flutter/foundation.dart'; + +import 'package:pshared/models/payment/payment.dart'; +import 'package:pshared/provider/organizations.dart'; +import 'package:pshared/provider/payment/quotation.dart'; +import 'package:pshared/provider/resource.dart'; +import 'package:pshared/service/payment/service.dart'; + + +class PaymentProvider extends ChangeNotifier { + late OrganizationsProvider _organization; + late QuotationProvider _quotation; + + Resource _payment = Resource(data: null, isLoading: false, error: null); + bool _isLoaded = false; + + void update(OrganizationsProvider organization, QuotationProvider quotation) { + _quotation = quotation; + _organization = organization; + } + + Payment? get payment => _payment.data; + bool get isLoading => _payment.isLoading; + Exception? get error => _payment.error; + bool get isReady => _isLoaded && !_payment.isLoading && _payment.error == null; + + void _setResource(Resource payment) { + _payment = payment; + notifyListeners(); + } + + Future pay({String? idempotencyKey, Map? metadata}) async { + if (!_organization.isOrganizationSet) throw StateError('Organization is not set'); + if (!_quotation.isReady) throw StateError('Quotation is not ready'); + final quoteRef = _quotation.quotation?.quoteRef; + if (quoteRef == null || quoteRef.isEmpty) { + throw StateError('Quotation reference is not set'); + } + + _setResource(_payment.copyWith(isLoading: true, error: null)); + try { + final response = await PaymentService.pay( + _organization.current.id, + quoteRef, + idempotencyKey: idempotencyKey, + metadata: metadata, + ); + _isLoaded = true; + _setResource(_payment.copyWith(data: response, isLoading: false, error: null)); + } catch (e) { + _setResource(_payment.copyWith( + data: null, + error: e is Exception ? e : Exception(e.toString()), + isLoading: false, + )); + } + return _payment.data; + } + + void reset() { + _setResource(Resource(data: null, isLoading: false, error: null)); + _isLoaded = false; + } +} diff --git a/frontend/pshared/lib/service/payment/service.dart b/frontend/pshared/lib/service/payment/service.dart new file mode 100644 index 0000000..a72b6f8 --- /dev/null +++ b/frontend/pshared/lib/service/payment/service.dart @@ -0,0 +1,35 @@ +import 'package:logging/logging.dart'; +import 'package:uuid/uuid.dart'; + +import 'package:pshared/api/requests/payment/initiate.dart'; +import 'package:pshared/api/responses/payment/payment.dart'; +import 'package:pshared/data/mapper/payment/payment_response.dart'; +import 'package:pshared/models/payment/payment.dart'; +import 'package:pshared/service/authorization/service.dart'; +import 'package:pshared/service/services.dart'; + + +class PaymentService { + static final _logger = Logger('service.payment'); + static const String _objectType = Services.payments; + + static Future pay( + String organizationRef, + String quotationRef, { + String? idempotencyKey, + Map? metadata, + }) async { + _logger.fine('Executing payment for quotation $quotationRef in $organizationRef'); + final request = InitiatePaymentRequest( + idempotencyKey: idempotencyKey ?? Uuid().v4(), + quoteRef: quotationRef, + metadata: metadata, + ); + final response = await AuthorizationService.getPOSTResponse( + _objectType, + '/by-quote/$organizationRef', + request.toJson(), + ); + return PaymentResponse.fromJson(response).payment.toDomain(); + } +} diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index e959c34..af111dd 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -13,6 +13,9 @@ import 'package:pshared/provider/permissions.dart'; import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/payment/amount.dart'; +import 'package:pshared/provider/payment/flow.dart'; +import 'package:pshared/provider/payment/provider.dart'; +import 'package:pshared/provider/payment/quotation.dart'; import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/payment/wallets.dart'; @@ -21,7 +24,7 @@ import 'package:pshared/service/payment/wallets.dart'; import 'package:pweb/app/app.dart'; import 'package:pweb/app/timeago.dart'; import 'package:pweb/providers/carousel.dart'; -import 'package:pweb/providers/mock_payment.dart'; +import 'package:pshared/models/payment/type.dart'; import 'package:pweb/providers/operatioins.dart'; import 'package:pweb/providers/two_factor.dart'; import 'package:pweb/providers/upload_history.dart'; @@ -90,7 +93,7 @@ void main() async { create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(), ), ChangeNotifierProvider( - create: (_) => MockPaymentProvider(), + create: (_) => PaymentFlowProvider(initialType: PaymentType.bankAccount), ), ChangeNotifierProvider( @@ -99,6 +102,22 @@ void main() async { ChangeNotifierProvider( create: (_) => PaymentAmountProvider(), ), + ChangeNotifierProxyProvider4( + create: (_) => QuotationProvider(), + update: (context, organization, payment, wallets, flow, provider) => provider!..update( + organization, + payment, + wallets, + flow, + ), + ), + ChangeNotifierProxyProvider2( + create: (_) => PaymentProvider(), + update: (context, organization, quotation, provider) => provider!..update( + organization, + quotation, + ), + ), ], child: const PayApp(), ), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/widget.dart index a88849e..7c23eaa 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/widget.dart @@ -1,13 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:pshared/provider/organizations.dart'; -import 'package:pshared/provider/payment/amount.dart'; -import 'package:pshared/provider/payment/flow.dart'; -import 'package:pshared/provider/payment/quotation.dart'; -import 'package:pshared/provider/payment/wallets.dart'; - import 'package:pweb/pages/dashboard/payouts/form.dart'; @@ -15,16 +7,5 @@ class PaymentFromWrappingWidget extends StatelessWidget { const PaymentFromWrappingWidget({super.key}); @override - Widget build(BuildContext context) => MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (_) => PaymentAmountProvider(), - ), - ChangeNotifierProxyProvider4( - create: (_) => QuotationProvider(), - update: (context, orgnization, payment, wallet, flow, provider) => provider!..update(orgnization, payment, wallet, flow), - ), - ], - child: const PaymentFormWidget(), - ); + Widget build(BuildContext context) => const PaymentFormWidget(); } diff --git a/frontend/pweb/lib/pages/payment_methods/page.dart b/frontend/pweb/lib/pages/payment_methods/page.dart index 870c02d..ea2631e 100644 --- a/frontend/pweb/lib/pages/payment_methods/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/page.dart @@ -8,6 +8,7 @@ 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/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:pshared/models/payment/wallet.dart'; @@ -38,16 +39,12 @@ class PaymentPage extends StatefulWidget { class _PaymentPageState extends State { late final TextEditingController _searchController; late final FocusNode _searchFocusNode; - late final PaymentFlowProvider _flowProvider; @override void initState() { super.initState(); _searchController = TextEditingController(); _searchFocusNode = FocusNode(); - _flowProvider = PaymentFlowProvider( - initialType: widget.initialPaymentType ?? PaymentType.bankAccount, - ); WidgetsBinding.instance.addPostFrameCallback((_) => _initializePaymentPage()); } @@ -56,16 +53,16 @@ class _PaymentPageState extends State { void dispose() { _searchController.dispose(); _searchFocusNode.dispose(); - _flowProvider.dispose(); super.dispose(); } void _initializePaymentPage() { + final flowProvider = context.read(); final methodsProvider = context.read(); - _handleWalletAutoSelection(methodsProvider); + _handleWalletAutoSelection(methodsProvider, flowProvider); final recipient = context.read().currentObject; - _flowProvider.syncWith( + flowProvider.syncWith( recipient: recipient, methodsProvider: methodsProvider, preferredType: widget.initialPaymentType, @@ -77,11 +74,12 @@ class _PaymentPageState extends State { } void _handleRecipientSelected(Recipient recipient) { + final flowProvider = context.read(); final recipientProvider = context.read(); final methodsProvider = context.read(); recipientProvider.setCurrentObject(recipient.id); - _flowProvider.reset( + flowProvider.reset( recipient: recipient, methodsProvider: methodsProvider, preferredType: widget.initialPaymentType, @@ -90,11 +88,12 @@ class _PaymentPageState extends State { } void _handleRecipientCleared() { + final flowProvider = context.read(); final recipientProvider = context.read(); final methodsProvider = context.read(); recipientProvider.setCurrentObject(null); - _flowProvider.reset( + flowProvider.reset( recipient: null, methodsProvider: methodsProvider, preferredType: widget.initialPaymentType, @@ -109,8 +108,18 @@ class _PaymentPageState extends State { } void _handleSendPayment() { - // TODO: Handle Payment logic - PosthogService.paymentInitiated(method: _flowProvider.selectedType); + final flowProvider = context.read(); + final paymentProvider = context.read(); + if (paymentProvider.isLoading) return; + + paymentProvider.pay().then((_) { + PosthogService.paymentInitiated(method: flowProvider.selectedType); + }).catchError((error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(error.toString())), + ); + }); } @override @@ -120,38 +129,41 @@ class _PaymentPageState extends State { final recipient = context.select( (provider) => provider.currentObject, ); + final flowProvider = context.watch(); - _flowProvider.syncWith( + flowProvider.syncWith( 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, - searchController: _searchController, - searchFocusNode: _searchFocusNode, - onSearchChanged: _handleSearchChanged, - onRecipientSelected: _handleRecipientSelected, - onRecipientCleared: _handleRecipientCleared, - onSend: _handleSendPayment, - ), + return PaymentPageBody( + onBack: widget.onBack, + fallbackDestination: widget.fallbackDestination, + recipient: recipient, + recipientProvider: recipientProvider, + methodsProvider: methodsProvider, + searchController: _searchController, + searchFocusNode: _searchFocusNode, + onSearchChanged: _handleSearchChanged, + onRecipientSelected: _handleRecipientSelected, + onRecipientCleared: _handleRecipientCleared, + onSend: _handleSendPayment, ); } - void _handleWalletAutoSelection(PaymentMethodsProvider methodsProvider) { + void _handleWalletAutoSelection(PaymentMethodsProvider methodsProvider, PaymentFlowProvider flowProvider) { final wallet = context.read().selectedWallet; if (wallet == null) return; final matchingMethod = _getPaymentMethodForWallet(wallet, methodsProvider); if (matchingMethod != null) { methodsProvider.setCurrentObject(matchingMethod.id); + flowProvider.syncWith( + recipient: context.read().currentObject, + methodsProvider: methodsProvider, + preferredType: widget.initialPaymentType, + ); } } @@ -169,4 +181,4 @@ class _PaymentPageState extends State { (method.description?.contains(wallet.walletUserID) ?? false), ); } -} \ No newline at end of file +} -- 2.49.1 From 75d5a512cddd1a37bfd1eb99050d32b1be7e22bb Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 26 Dec 2025 14:37:45 +0300 Subject: [PATCH 3/5] Removed manual syncWith/reset calls and added an update in PaymentFlowProvider --- .../pshared/lib/provider/payment/flow.dart | 47 ++++++++++--------- frontend/pweb/lib/main.dart | 9 ++-- .../pweb/lib/pages/payment_methods/page.dart | 41 ++-------------- 3 files changed, 35 insertions(+), 62 deletions(-) diff --git a/frontend/pshared/lib/provider/payment/flow.dart b/frontend/pshared/lib/provider/payment/flow.dart index 12b909f..e045149 100644 --- a/frontend/pshared/lib/provider/payment/flow.dart +++ b/frontend/pshared/lib/provider/payment/flow.dart @@ -3,18 +3,22 @@ import 'package:flutter/foundation.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/pmethods.dart'; class PaymentFlowProvider extends ChangeNotifier { PaymentType _selectedType; + PaymentType? _preferredType; PaymentMethodData? _manualPaymentData; MethodMap _availableTypes = {}; Recipient? _recipient; PaymentFlowProvider({ required PaymentType initialType, - }) : _selectedType = initialType; + PaymentType? preferredType, + }) : _selectedType = initialType, + _preferredType = preferredType ?? initialType; PaymentType get selectedType => _selectedType; PaymentMethodData? get manualPaymentData => _manualPaymentData; @@ -29,30 +33,17 @@ class PaymentFlowProvider extends ChangeNotifier { PaymentMethodData? get selectedPaymentData => hasRecipient ? _availableTypes[_selectedType] : _manualPaymentData; - void syncWith({ - required Recipient? recipient, - required PaymentMethodsProvider methodsProvider, - PaymentType? preferredType, - }) => + void update( + RecipientsProvider recipientsProvider, + PaymentMethodsProvider methodsProvider, + ) => _applyState( - recipient: recipient, - availableTypes: methodsProvider.availableTypesForRecipient(recipient), - preferredType: preferredType, + recipient: recipientsProvider.currentObject, + availableTypes: methodsProvider.availableTypesForRecipient(recipientsProvider.currentObject), + preferredType: _preferredType, forceResetManualData: false, ); - void reset({ - required Recipient? recipient, - required PaymentMethodsProvider methodsProvider, - PaymentType? preferredType, - }) => - _applyState( - recipient: recipient, - availableTypes: methodsProvider.availableTypesForRecipient(recipient), - preferredType: preferredType, - forceResetManualData: true, - ); - void selectType(PaymentType type, {bool resetManualData = false}) { if (_selectedType == type && (!resetManualData || _manualPaymentData == null)) { return; @@ -70,6 +61,20 @@ class PaymentFlowProvider extends ChangeNotifier { notifyListeners(); } + void setPreferredType(PaymentType? preferredType) { + if (_preferredType == preferredType) { + return; + } + + _preferredType = preferredType; + _applyState( + recipient: _recipient, + availableTypes: _availableTypes, + preferredType: _preferredType, + forceResetManualData: false, + ); + } + PaymentType _resolveSelectedType({ required Recipient? recipient, required MethodMap availableTypes, diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index af111dd..a93a4f9 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -20,11 +20,11 @@ import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/payment/wallets.dart'; import 'package:pshared/service/payment/wallets.dart'; +import 'package:pshared/models/payment/type.dart'; import 'package:pweb/app/app.dart'; import 'package:pweb/app/timeago.dart'; import 'package:pweb/providers/carousel.dart'; -import 'package:pshared/models/payment/type.dart'; import 'package:pweb/providers/operatioins.dart'; import 'package:pweb/providers/two_factor.dart'; import 'package:pweb/providers/upload_history.dart'; @@ -92,10 +92,13 @@ void main() async { ChangeNotifierProvider( create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(), ), - ChangeNotifierProvider( + ChangeNotifierProxyProvider2( create: (_) => PaymentFlowProvider(initialType: PaymentType.bankAccount), + update: (context, recipients, methods, provider) => provider!..update( + recipients, + methods, + ), ), - ChangeNotifierProvider( create: (_) => OperationProvider(OperationService())..loadOperations(), ), diff --git a/frontend/pweb/lib/pages/payment_methods/page.dart b/frontend/pweb/lib/pages/payment_methods/page.dart index ea2631e..153c5b1 100644 --- a/frontend/pweb/lib/pages/payment_methods/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/page.dart @@ -14,7 +14,6 @@ import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/provider/payment/wallets.dart'; - import 'package:pweb/pages/payment_methods/payment_page/body.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/services/posthog.dart'; @@ -59,14 +58,8 @@ class _PaymentPageState extends State { void _initializePaymentPage() { final flowProvider = context.read(); final methodsProvider = context.read(); - _handleWalletAutoSelection(methodsProvider, flowProvider); - - final recipient = context.read().currentObject; - flowProvider.syncWith( - recipient: recipient, - methodsProvider: methodsProvider, - preferredType: widget.initialPaymentType, - ); + flowProvider.setPreferredType(widget.initialPaymentType); + _handleWalletAutoSelection(methodsProvider); } void _handleSearchChanged(String query) { @@ -74,30 +67,14 @@ class _PaymentPageState extends State { } void _handleRecipientSelected(Recipient recipient) { - final flowProvider = context.read(); final recipientProvider = context.read(); - final methodsProvider = context.read(); - recipientProvider.setCurrentObject(recipient.id); - flowProvider.reset( - recipient: recipient, - methodsProvider: methodsProvider, - preferredType: widget.initialPaymentType, - ); _clearSearchField(); } void _handleRecipientCleared() { - final flowProvider = context.read(); final recipientProvider = context.read(); - final methodsProvider = context.read(); - recipientProvider.setCurrentObject(null); - flowProvider.reset( - recipient: null, - methodsProvider: methodsProvider, - preferredType: widget.initialPaymentType, - ); _clearSearchField(); } @@ -129,13 +106,6 @@ class _PaymentPageState extends State { final recipient = context.select( (provider) => provider.currentObject, ); - final flowProvider = context.watch(); - - flowProvider.syncWith( - recipient: recipient, - methodsProvider: methodsProvider, - preferredType: recipient != null ? widget.initialPaymentType : null, - ); return PaymentPageBody( onBack: widget.onBack, @@ -152,18 +122,13 @@ class _PaymentPageState extends State { ); } - void _handleWalletAutoSelection(PaymentMethodsProvider methodsProvider, PaymentFlowProvider flowProvider) { + void _handleWalletAutoSelection(PaymentMethodsProvider methodsProvider) { final wallet = context.read().selectedWallet; if (wallet == null) return; final matchingMethod = _getPaymentMethodForWallet(wallet, methodsProvider); if (matchingMethod != null) { methodsProvider.setCurrentObject(matchingMethod.id); - flowProvider.syncWith( - recipient: context.read().currentObject, - methodsProvider: methodsProvider, - preferredType: widget.initialPaymentType, - ); } } -- 2.49.1 From f3396301157393fa369f18668028e28a7a86ec5e Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 26 Dec 2025 15:11:47 +0300 Subject: [PATCH 4/5] Moved all the payment data preparation logic from the paymentFlowProvider to the payment and walletproviders --- .../pshared/lib/provider/payment/flow.dart | 44 +++++++++++++++---- .../pshared/lib/provider/payment/wallets.dart | 25 +++++++++-- frontend/pweb/lib/main.dart | 2 +- .../pweb/lib/pages/payment_methods/page.dart | 32 +------------- .../payment_methods/payment_page/body.dart | 5 ++- .../payment_methods/payment_page/content.dart | 8 ++-- .../payment_page/method_selector.dart | 12 ++--- .../payment_methods/payment_page/page.dart | 8 ++-- frontend/pweb/lib/utils/payment/dropdown.dart | 34 ++++++-------- 9 files changed, 91 insertions(+), 79 deletions(-) diff --git a/frontend/pshared/lib/provider/payment/flow.dart b/frontend/pshared/lib/provider/payment/flow.dart index e045149..1f80552 100644 --- a/frontend/pshared/lib/provider/payment/flow.dart +++ b/frontend/pshared/lib/provider/payment/flow.dart @@ -1,6 +1,8 @@ +import 'package:collection/collection.dart'; import 'package:flutter/foundation.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/provider.dart'; @@ -11,7 +13,7 @@ class PaymentFlowProvider extends ChangeNotifier { PaymentType _selectedType; PaymentType? _preferredType; PaymentMethodData? _manualPaymentData; - MethodMap _availableTypes = {}; + List _recipientMethods = []; Recipient? _recipient; PaymentFlowProvider({ @@ -23,15 +25,22 @@ class PaymentFlowProvider extends ChangeNotifier { PaymentType get selectedType => _selectedType; PaymentMethodData? get manualPaymentData => _manualPaymentData; Recipient? get recipient => _recipient; + PaymentMethod? get selectedMethod => hasRecipient + ? _recipientMethods.firstWhereOrNull((method) => method.type == _selectedType) + : null; bool get hasRecipient => _recipient != null; MethodMap get availableTypes => hasRecipient - ? _availableTypes + ? _buildAvailableTypes(_recipientMethods) : {for (final type in PaymentType.values) type: null}; PaymentMethodData? get selectedPaymentData => - hasRecipient ? _availableTypes[_selectedType] : _manualPaymentData; + hasRecipient ? selectedMethod?.data : _manualPaymentData; + + List get methodsForRecipient => hasRecipient + ? List.unmodifiable(_recipientMethods) + : const []; void update( RecipientsProvider recipientsProvider, @@ -39,12 +48,16 @@ class PaymentFlowProvider extends ChangeNotifier { ) => _applyState( recipient: recipientsProvider.currentObject, - availableTypes: methodsProvider.availableTypesForRecipient(recipientsProvider.currentObject), + methods: methodsProvider.methodsForRecipient(recipientsProvider.currentObject), preferredType: _preferredType, forceResetManualData: false, ); void selectType(PaymentType type, {bool resetManualData = false}) { + if (hasRecipient && !availableTypes.containsKey(type)) { + return; + } + if (_selectedType == type && (!resetManualData || _manualPaymentData == null)) { return; } @@ -69,7 +82,7 @@ class PaymentFlowProvider extends ChangeNotifier { _preferredType = preferredType; _applyState( recipient: _recipient, - availableTypes: _availableTypes, + methods: _recipientMethods, preferredType: _preferredType, forceResetManualData: false, ); @@ -101,10 +114,11 @@ class PaymentFlowProvider extends ChangeNotifier { void _applyState({ required Recipient? recipient, - required MethodMap availableTypes, + required List methods, required PaymentType? preferredType, required bool forceResetManualData, }) { + final availableTypes = _buildAvailableTypes(methods); final resolvedType = _resolveSelectedType( recipient: recipient, availableTypes: availableTypes, @@ -118,8 +132,8 @@ class PaymentFlowProvider extends ChangeNotifier { hasChanges = true; } - if (!mapEquals(_availableTypes, availableTypes)) { - _availableTypes = availableTypes; + if (!_hasSameMethods(methods)) { + _recipientMethods = methods; hasChanges = true; } @@ -135,4 +149,18 @@ class PaymentFlowProvider extends ChangeNotifier { if (hasChanges) notifyListeners(); } + + MethodMap _buildAvailableTypes(List methods) => { + for (final method in methods) method.type: method.data, + }; + + bool _hasSameMethods(List methods) { + if (_recipientMethods.length != methods.length) return false; + for (var i = 0; i < methods.length; i++) { + final current = _recipientMethods[i]; + final next = methods[i]; + if (current.id != next.id || current.updatedAt != next.updatedAt) return false; + } + return true; + } } diff --git a/frontend/pshared/lib/provider/payment/wallets.dart b/frontend/pshared/lib/provider/payment/wallets.dart index 4629cec..2e655ef 100644 --- a/frontend/pshared/lib/provider/payment/wallets.dart +++ b/frontend/pshared/lib/provider/payment/wallets.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:pshared/models/payment/wallet.dart'; @@ -38,10 +39,7 @@ class WalletsProvider with ChangeNotifier { throw Exception('update wallet is not implemented'); } - void selectWallet(Wallet wallet) { - _selectedWallet = wallet; - notifyListeners(); - } + void selectWallet(Wallet wallet) => _setSelectedWallet(wallet); Future loadWalletsWithBalances() async { _setResource(_resource.copyWith(isLoading: true, error: null)); @@ -98,6 +96,25 @@ class WalletsProvider with ChangeNotifier { void _setResource(Resource> newResource) { _resource = newResource; + _selectedWallet = _resolveSelectedWallet(_selectedWallet, wallets); + notifyListeners(); + } + + Wallet? _resolveSelectedWallet(Wallet? current, List available) { + if (available.isEmpty) return null; + final currentId = current?.id; + if (currentId != null) { + final existing = available.firstWhereOrNull((wallet) => wallet.id == currentId); + if (existing != null) return existing; + } + return available.firstWhereOrNull((wallet) => !wallet.isHidden) ?? available.first; + } + + void _setSelectedWallet(Wallet wallet) { + if (_selectedWallet?.id == wallet.id && _selectedWallet?.isHidden == wallet.isHidden) { + return; + } + _selectedWallet = wallet; notifyListeners(); } } diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index a93a4f9..4ab6802 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -19,8 +19,8 @@ import 'package:pshared/provider/payment/quotation.dart'; import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/payment/wallets.dart'; -import 'package:pshared/service/payment/wallets.dart'; import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/service/payment/wallets.dart'; import 'package:pweb/app/app.dart'; import 'package:pweb/app/timeago.dart'; diff --git a/frontend/pweb/lib/pages/payment_methods/page.dart b/frontend/pweb/lib/pages/payment_methods/page.dart index 153c5b1..5598647 100644 --- a/frontend/pweb/lib/pages/payment_methods/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/page.dart @@ -1,17 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; - import 'package:provider/provider.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/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:pshared/models/payment/wallet.dart'; import 'package:pshared/provider/payment/wallets.dart'; import 'package:pweb/pages/payment_methods/payment_page/body.dart'; @@ -57,9 +53,7 @@ class _PaymentPageState extends State { void _initializePaymentPage() { final flowProvider = context.read(); - final methodsProvider = context.read(); flowProvider.setPreferredType(widget.initialPaymentType); - _handleWalletAutoSelection(methodsProvider); } void _handleSearchChanged(String query) { @@ -113,6 +107,7 @@ class _PaymentPageState extends State { recipient: recipient, recipientProvider: recipientProvider, methodsProvider: methodsProvider, + onWalletSelected: context.read().selectWallet, searchController: _searchController, searchFocusNode: _searchFocusNode, onSearchChanged: _handleSearchChanged, @@ -121,29 +116,4 @@ class _PaymentPageState extends State { onSend: _handleSendPayment, ); } - - void _handleWalletAutoSelection(PaymentMethodsProvider methodsProvider) { - final wallet = context.read().selectedWallet; - if (wallet == null) return; - - final matchingMethod = _getPaymentMethodForWallet(wallet, methodsProvider); - if (matchingMethod != null) { - methodsProvider.setCurrentObject(matchingMethod.id); - } - } - - 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), - ); - } } diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/body.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/body.dart index 6d58a6a..9741606 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/body.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/body.dart @@ -1,6 +1,7 @@ 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'; @@ -16,6 +17,7 @@ class PaymentPageBody extends StatelessWidget { final Recipient? recipient; final RecipientsProvider recipientProvider; final PaymentMethodsProvider methodsProvider; + final ValueChanged onWalletSelected; final PayoutDestination fallbackDestination; final TextEditingController searchController; final FocusNode searchFocusNode; @@ -30,6 +32,7 @@ class PaymentPageBody extends StatelessWidget { required this.recipient, required this.recipientProvider, required this.methodsProvider, + required this.onWalletSelected, required this.fallbackDestination, required this.searchController, required this.searchFocusNode, @@ -57,7 +60,7 @@ class PaymentPageBody extends StatelessWidget { onBack: onBack, recipient: recipient, recipientProvider: recipientProvider, - methodsProvider: methodsProvider, + onWalletSelected: onWalletSelected, fallbackDestination: fallbackDestination, searchController: searchController, searchFocusNode: searchFocusNode, diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/content.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/content.dart index ebb9f3e..aa274cf 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/content.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/content.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:pshared/models/payment/wallet.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/payment_page/back_button.dart'; @@ -22,7 +22,7 @@ class PaymentPageContent extends StatelessWidget { final ValueChanged? onBack; final Recipient? recipient; final RecipientsProvider recipientProvider; - final PaymentMethodsProvider methodsProvider; + final ValueChanged onWalletSelected; final PayoutDestination fallbackDestination; final TextEditingController searchController; final FocusNode searchFocusNode; @@ -36,7 +36,7 @@ class PaymentPageContent extends StatelessWidget { required this.onBack, required this.recipient, required this.recipientProvider, - required this.methodsProvider, + required this.onWalletSelected, required this.fallbackDestination, required this.searchController, required this.searchFocusNode, @@ -77,7 +77,7 @@ class PaymentPageContent extends StatelessWidget { SectionTitle(loc.sourceOfFunds), SizedBox(height: dimensions.paddingSmall), PaymentMethodSelector( - onMethodChanged: (m) => methodsProvider.setCurrentObject(m.id), + onMethodChanged: onWalletSelected, ), SizedBox(height: dimensions.paddingXLarge), RecipientSection( diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart index 42404c0..ba4c3fc 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart @@ -17,9 +17,11 @@ class PaymentMethodSelector extends StatelessWidget { }); @override - Widget build(BuildContext context) => Consumer(builder:(context, provider, _) => PaymentMethodDropdown( - methods: provider.wallets, - initialValue: provider.selectedWallet, - onChanged: onMethodChanged, - )); + Widget build(BuildContext context) => Consumer( + builder: (context, provider, _) => PaymentMethodDropdown( + methods: provider.wallets, + selectedMethod: provider.selectedWallet, + onChanged: onMethodChanged, + ), + ); } diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart index 7d23770..03d4f90 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:pshared/models/payment/wallet.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/dashboard/payouts/widget.dart'; @@ -22,7 +22,7 @@ class PaymentPageContent extends StatelessWidget { final ValueChanged? onBack; final Recipient? recipient; final RecipientsProvider recipientProvider; - final PaymentMethodsProvider methodsProvider; + final ValueChanged onWalletSelected; final PayoutDestination fallbackDestination; final TextEditingController searchController; final FocusNode searchFocusNode; @@ -36,7 +36,7 @@ class PaymentPageContent extends StatelessWidget { required this.onBack, required this.recipient, required this.recipientProvider, - required this.methodsProvider, + required this.onWalletSelected, required this.fallbackDestination, required this.searchController, required this.searchFocusNode, @@ -77,7 +77,7 @@ class PaymentPageContent extends StatelessWidget { SectionTitle(loc.sourceOfFunds), SizedBox(height: dimensions.paddingSmall), PaymentMethodSelector( - onMethodChanged: (m) => methodsProvider.setCurrentObject(m.id), + onMethodChanged: onWalletSelected, ), SizedBox(height: dimensions.paddingXLarge), RecipientSection( diff --git a/frontend/pweb/lib/utils/payment/dropdown.dart b/frontend/pweb/lib/utils/payment/dropdown.dart index 25a132b..eb237d0 100644 --- a/frontend/pweb/lib/utils/payment/dropdown.dart +++ b/frontend/pweb/lib/utils/payment/dropdown.dart @@ -8,40 +8,27 @@ import 'package:pweb/pages/payment_methods/icon.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -class PaymentMethodDropdown extends StatefulWidget { +class PaymentMethodDropdown extends StatelessWidget { final List methods; final ValueChanged onChanged; - final Wallet? initialValue; + final Wallet? selectedMethod; const PaymentMethodDropdown({ super.key, required this.methods, required this.onChanged, - this.initialValue, + this.selectedMethod, }); - @override - State createState() => _PaymentMethodDropdownState(); -} - -class _PaymentMethodDropdownState extends State { - late Wallet _selectedMethod; - - @override - void initState() { - super.initState(); - _selectedMethod = widget.initialValue ?? widget.methods.first; - } - @override Widget build(BuildContext context) => DropdownButtonFormField( dropdownColor: Theme.of(context).colorScheme.onSecondary, - initialValue: _selectedMethod, + value: _getSelectedMethod(), decoration: InputDecoration( labelText: AppLocalizations.of(context)!.whereGetMoney, border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), ), - items: widget.methods.map((method) => DropdownMenuItem( + items: methods.map((method) => DropdownMenuItem( value: method, child: Row( children: [ @@ -53,9 +40,14 @@ class _PaymentMethodDropdownState extends State { )).toList(), onChanged: (value) { if (value != null) { - setState(() => _selectedMethod = value); - widget.onChanged(value); + onChanged(value); } }, ); -} \ No newline at end of file + + Wallet? _getSelectedMethod() { + if (selectedMethod != null) return selectedMethod; + if (methods.isEmpty) return null; + return methods.first; + } +} -- 2.49.1 From ad3d44f1371cefb7943ec7a6a9ed736d0fcee7c2 Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 26 Dec 2025 19:26:19 +0300 Subject: [PATCH 5/5] Fixes for build --- .../lib/data/dto/payment/intent/customer.dart | 41 +++++++++++ .../lib/data/dto/payment/intent/payment.dart | 3 + .../data/mapper/payment/intent/customer.dart | 33 +++++++++ .../data/mapper/payment/intent/payment.dart | 36 +++++---- .../pshared/lib/models/payment/customer.dart | 25 +++++++ .../pshared/lib/models/payment/intent.dart | 3 + .../lib/provider/payment/quotation.dart | 73 ++++++++++++++++++- frontend/pweb/lib/main.dart | 10 +-- 8 files changed, 198 insertions(+), 26 deletions(-) create mode 100644 frontend/pshared/lib/data/dto/payment/intent/customer.dart create mode 100644 frontend/pshared/lib/data/mapper/payment/intent/customer.dart create mode 100644 frontend/pshared/lib/models/payment/customer.dart diff --git a/frontend/pshared/lib/data/dto/payment/intent/customer.dart b/frontend/pshared/lib/data/dto/payment/intent/customer.dart new file mode 100644 index 0000000..a327318 --- /dev/null +++ b/frontend/pshared/lib/data/dto/payment/intent/customer.dart @@ -0,0 +1,41 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'customer.g.dart'; + + +@JsonSerializable() +class CustomerDTO { + final String id; + + @JsonKey(name: 'first_name') + final String? firstName; + + @JsonKey(name: 'middle_name') + final String? middleName; + + @JsonKey(name: 'last_name') + final String? lastName; + + final String? ip; + final String? zip; + final String? country; + final String? state; + final String? city; + final String? address; + + const CustomerDTO({ + required this.id, + this.firstName, + this.middleName, + this.lastName, + this.ip, + this.zip, + this.country, + this.state, + this.city, + this.address, + }); + + factory CustomerDTO.fromJson(Map json) => _$CustomerDTOFromJson(json); + Map toJson() => _$CustomerDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/payment/intent/payment.dart b/frontend/pshared/lib/data/dto/payment/intent/payment.dart index 5ce9c38..34cf653 100644 --- a/frontend/pshared/lib/data/dto/payment/intent/payment.dart +++ b/frontend/pshared/lib/data/dto/payment/intent/payment.dart @@ -1,6 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pshared/data/dto/payment/endpoint.dart'; +import 'package:pshared/data/dto/payment/intent/customer.dart'; import 'package:pshared/data/dto/payment/intent/fx.dart'; import 'package:pshared/data/dto/payment/money.dart'; @@ -20,6 +21,7 @@ class PaymentIntentDTO { final String? settlementMode; final Map? attributes; + final CustomerDTO? customer; const PaymentIntentDTO({ this.kind, @@ -29,6 +31,7 @@ class PaymentIntentDTO { this.fx, this.settlementMode, this.attributes, + this.customer, }); factory PaymentIntentDTO.fromJson(Map json) => _$PaymentIntentDTOFromJson(json); diff --git a/frontend/pshared/lib/data/mapper/payment/intent/customer.dart b/frontend/pshared/lib/data/mapper/payment/intent/customer.dart new file mode 100644 index 0000000..e6d3e00 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/payment/intent/customer.dart @@ -0,0 +1,33 @@ +import 'package:pshared/data/dto/payment/intent/customer.dart'; +import 'package:pshared/models/payment/customer.dart'; + + +extension CustomerMapper on Customer { + CustomerDTO toDTO() => CustomerDTO( + id: id, + firstName: firstName, + middleName: middleName, + lastName: lastName, + ip: ip, + zip: zip, + country: country, + state: state, + city: city, + address: address, + ); +} + +extension CustomerDTOMapper on CustomerDTO { + Customer toDomain() => Customer( + id: id, + firstName: firstName, + middleName: middleName, + lastName: lastName, + ip: ip, + zip: zip, + country: country, + state: state, + city: city, + address: address, + ); +} diff --git a/frontend/pshared/lib/data/mapper/payment/intent/payment.dart b/frontend/pshared/lib/data/mapper/payment/intent/payment.dart index 0086294..06286f3 100644 --- a/frontend/pshared/lib/data/mapper/payment/intent/payment.dart +++ b/frontend/pshared/lib/data/mapper/payment/intent/payment.dart @@ -1,30 +1,34 @@ import 'package:pshared/data/dto/payment/intent/payment.dart'; import 'package:pshared/data/mapper/payment/payment.dart'; import 'package:pshared/data/mapper/payment/enums.dart'; +import 'package:pshared/data/mapper/payment/intent/customer.dart'; import 'package:pshared/data/mapper/payment/intent/fx.dart'; import 'package:pshared/data/mapper/payment/money.dart'; import 'package:pshared/models/payment/intent.dart'; + extension PaymentIntentMapper on PaymentIntent { PaymentIntentDTO toDTO() => PaymentIntentDTO( - kind: paymentKindToValue(kind), - source: source?.toDTO(), - destination: destination?.toDTO(), - amount: amount?.toDTO(), - fx: fx?.toDTO(), - settlementMode: settlementModeToValue(settlementMode), - attributes: attributes, - ); + kind: paymentKindToValue(kind), + source: source?.toDTO(), + destination: destination?.toDTO(), + amount: amount?.toDTO(), + fx: fx?.toDTO(), + settlementMode: settlementModeToValue(settlementMode), + attributes: attributes, + customer: customer?.toDTO(), + ); } extension PaymentIntentDTOMapper on PaymentIntentDTO { PaymentIntent toDomain() => PaymentIntent( - kind: paymentKindFromValue(kind), - source: source?.toDomain(), - destination: destination?.toDomain(), - amount: amount?.toDomain(), - fx: fx?.toDomain(), - settlementMode: settlementModeFromValue(settlementMode), - attributes: attributes, - ); + kind: paymentKindFromValue(kind), + source: source?.toDomain(), + destination: destination?.toDomain(), + amount: amount?.toDomain(), + fx: fx?.toDomain(), + settlementMode: settlementModeFromValue(settlementMode), + attributes: attributes, + customer: customer?.toDomain(), + ); } diff --git a/frontend/pshared/lib/models/payment/customer.dart b/frontend/pshared/lib/models/payment/customer.dart new file mode 100644 index 0000000..726601f --- /dev/null +++ b/frontend/pshared/lib/models/payment/customer.dart @@ -0,0 +1,25 @@ +class Customer { + final String id; + final String? firstName; + final String? middleName; + final String? lastName; + final String? ip; + final String? zip; + final String? country; + final String? state; + final String? city; + final String? address; + + const Customer({ + required this.id, + this.firstName, + this.middleName, + this.lastName, + this.ip, + this.zip, + this.country, + this.state, + this.city, + this.address, + }); +} diff --git a/frontend/pshared/lib/models/payment/intent.dart b/frontend/pshared/lib/models/payment/intent.dart index 943c61e..24278b1 100644 --- a/frontend/pshared/lib/models/payment/intent.dart +++ b/frontend/pshared/lib/models/payment/intent.dart @@ -1,5 +1,6 @@ import 'package:pshared/models/payment/fx/intent.dart'; import 'package:pshared/models/payment/kind.dart'; +import 'package:pshared/models/payment/customer.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/money.dart'; import 'package:pshared/models/payment/settlement_mode.dart'; @@ -13,6 +14,7 @@ class PaymentIntent { final FxIntent? fx; final SettlementMode settlementMode; final Map? attributes; + final Customer? customer; const PaymentIntent({ this.kind = PaymentKind.unspecified, @@ -22,5 +24,6 @@ class PaymentIntent { this.fx, this.settlementMode = SettlementMode.unspecified, this.attributes, + this.customer, }); } diff --git a/frontend/pshared/lib/provider/payment/quotation.dart b/frontend/pshared/lib/provider/payment/quotation.dart index d093f49..7141de9 100644 --- a/frontend/pshared/lib/provider/payment/quotation.dart +++ b/frontend/pshared/lib/provider/payment/quotation.dart @@ -1,23 +1,30 @@ import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; + import 'package:uuid/uuid.dart'; import 'package:pshared/api/requests/payment/quote.dart'; import 'package:pshared/data/mapper/payment/intent/payment.dart'; import 'package:pshared/models/asset.dart'; import 'package:pshared/models/payment/currency_pair.dart'; +import 'package:pshared/models/payment/customer.dart'; import 'package:pshared/models/payment/fx/intent.dart'; import 'package:pshared/models/payment/fx/side.dart'; import 'package:pshared/models/payment/kind.dart'; import 'package:pshared/models/payment/methods/managed_wallet.dart'; +import 'package:pshared/models/payment/methods/type.dart'; import 'package:pshared/models/payment/money.dart'; import 'package:pshared/models/payment/settlement_mode.dart'; import 'package:pshared/models/payment/intent.dart'; import 'package:pshared/models/payment/quote.dart'; +import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/payment/amount.dart'; import 'package:pshared/provider/payment/flow.dart'; import 'package:pshared/provider/payment/wallets.dart'; +import 'package:pshared/provider/recipient/provider.dart'; +import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/payment/quotation.dart'; import 'package:pshared/utils/currency.dart'; @@ -33,10 +40,17 @@ class QuotationProvider extends ChangeNotifier { PaymentAmountProvider payment, WalletsProvider wallets, PaymentFlowProvider flow, + RecipientsProvider recipients, + PaymentMethodsProvider methods, ) { _organizations = venue; - final destination = flow.selectedPaymentData; - if ((wallets.selectedWallet != null) && (destination != null)) { + final t = flow.selectedType; + final method = methods.methods.firstWhereOrNull((m) => m.type == t); + if ((wallets.selectedWallet != null) && (method != null)) { + final customer = _buildCustomer( + recipient: recipients.currentObject, + method: method, + ); getQuotation(PaymentIntent( kind: PaymentKind.payout, amount: Money( @@ -44,7 +58,7 @@ class QuotationProvider extends ChangeNotifier { // TODO: adapt to possible other sources currency: currencyCodeToString(wallets.selectedWallet!.currency), ), - destination: destination, + destination: method.data, source: ManagedWalletPaymentMethod( managedWalletRef: wallets.selectedWallet!.id, ), @@ -56,6 +70,7 @@ class QuotationProvider extends ChangeNotifier { side: FxSide.sellBaseBuyQuote, ), settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource, + customer: customer, )); } } @@ -68,6 +83,58 @@ class QuotationProvider extends ChangeNotifier { Asset? get total => quotation == null ? null : createAsset(quotation!.debitAmount!.currency, quotation!.debitAmount!.amount); Asset? get recipientGets => quotation == null ? null : createAsset(quotation!.expectedSettlementAmount!.currency, quotation!.expectedSettlementAmount!.amount); + Customer _buildCustomer({ + required Recipient? recipient, + required PaymentMethod method, + }) { + final name = _resolveCustomerName(method, recipient); + String? firstName; + String? middleName; + String? lastName; + + if (name != null && name.isNotEmpty) { + final parts = name.split(RegExp(r'\s+')); + if (parts.length == 1) { + firstName = parts.first; + } else if (parts.length == 2) { + firstName = parts.first; + lastName = parts.last; + } else { + firstName = parts.first; + lastName = parts.last; + middleName = parts.sublist(1, parts.length - 1).join(' '); + } + } + + return Customer( + id: recipient?.id ?? method.recipientRef, + firstName: firstName, + middleName: middleName, + lastName: lastName, + country: method.cardData?.country, + ); + } + + String? _resolveCustomerName(PaymentMethod method, Recipient? recipient) { + final card = method.cardData; + if (card != null) { + return '${card.firstName} ${card.lastName}'.trim(); + } + + final iban = method.ibanData; + if (iban != null && iban.accountHolder.trim().isNotEmpty) { + return iban.accountHolder.trim(); + } + + final bank = method.bankAccountData; + if (bank != null && bank.recipientName.trim().isNotEmpty) { + return bank.recipientName.trim(); + } + + final recipientName = recipient?.name.trim(); + return recipientName?.isNotEmpty == true ? recipientName : null; + } + void _setResource(Resource quotation) { _quotation = quotation; notifyListeners(); diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 4ab6802..2c6c34d 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -105,14 +105,10 @@ void main() async { ChangeNotifierProvider( create: (_) => PaymentAmountProvider(), ), - ChangeNotifierProxyProvider4( + ChangeNotifierProxyProvider6( create: (_) => QuotationProvider(), - update: (context, organization, payment, wallets, flow, provider) => provider!..update( - organization, - payment, - wallets, - flow, - ), + update: (_, organization, payment, wallet, flow, recipients, methods, provider) => + provider!..update(organization, payment, wallet, flow, recipients, methods), ), ChangeNotifierProxyProvider2( create: (_) => PaymentProvider(), -- 2.49.1