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/flow.dart b/frontend/pshared/lib/provider/payment/flow.dart index e4f3f73..1f80552 100644 --- a/frontend/pshared/lib/provider/payment/flow.dart +++ b/frontend/pshared/lib/provider/payment/flow.dart @@ -1,73 +1,63 @@ +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'; +import 'package:pshared/provider/recipient/pmethods.dart'; class PaymentFlowProvider extends ChangeNotifier { PaymentType _selectedType; + PaymentType? _preferredType; PaymentMethodData? _manualPaymentData; + List _recipientMethods = []; + Recipient? _recipient; PaymentFlowProvider({ required PaymentType initialType, - }) : _selectedType = initialType; + PaymentType? preferredType, + }) : _selectedType = initialType, + _preferredType = preferredType ?? initialType; PaymentType get selectedType => _selectedType; PaymentMethodData? get manualPaymentData => _manualPaymentData; + Recipient? get recipient => _recipient; + PaymentMethod? get selectedMethod => hasRecipient + ? _recipientMethods.firstWhereOrNull((method) => method.type == _selectedType) + : null; - void sync({ - required Recipient? recipient, - required MethodMap availableTypes, - PaymentType? preferredType, - }) { - final resolvedType = _resolveSelectedType( - recipient: recipient, - availableTypes: availableTypes, - preferredType: preferredType, - ); + bool get hasRecipient => _recipient != null; - var hasChanges = false; - if (resolvedType != _selectedType) { - _selectedType = resolvedType; - hasChanges = true; - } + MethodMap get availableTypes => hasRecipient + ? _buildAvailableTypes(_recipientMethods) + : {for (final type in PaymentType.values) type: null}; - if (recipient != null && _manualPaymentData != null) { - _manualPaymentData = null; - hasChanges = true; - } + PaymentMethodData? get selectedPaymentData => + hasRecipient ? selectedMethod?.data : _manualPaymentData; - if (hasChanges) notifyListeners(); - } + List get methodsForRecipient => hasRecipient + ? List.unmodifiable(_recipientMethods) + : const []; - void reset({ - required Recipient? recipient, - required MethodMap availableTypes, - 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(); - } + void update( + RecipientsProvider recipientsProvider, + PaymentMethodsProvider methodsProvider, + ) => + _applyState( + recipient: 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; } @@ -84,6 +74,20 @@ class PaymentFlowProvider extends ChangeNotifier { notifyListeners(); } + void setPreferredType(PaymentType? preferredType) { + if (_preferredType == preferredType) { + return; + } + + _preferredType = preferredType; + _applyState( + recipient: _recipient, + methods: _recipientMethods, + preferredType: _preferredType, + forceResetManualData: false, + ); + } + PaymentType _resolveSelectedType({ required Recipient? recipient, required MethodMap availableTypes, @@ -107,4 +111,56 @@ class PaymentFlowProvider extends ChangeNotifier { return availableTypes.keys.first; } + + void _applyState({ + required Recipient? recipient, + required List methods, + required PaymentType? preferredType, + required bool forceResetManualData, + }) { + final availableTypes = _buildAvailableTypes(methods); + final resolvedType = _resolveSelectedType( + recipient: recipient, + availableTypes: availableTypes, + preferredType: preferredType, + ); + + var hasChanges = false; + + if (_recipient != recipient) { + _recipient = recipient; + hasChanges = true; + } + + if (!_hasSameMethods(methods)) { + _recipientMethods = methods; + hasChanges = true; + } + + if (resolvedType != _selectedType) { + _selectedType = resolvedType; + hasChanges = true; + } + + if ((recipient != null || forceResetManualData) && _manualPaymentData != null) { + _manualPaymentData = null; + hasChanges = true; + } + + 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/quotation.dart b/frontend/pshared/lib/provider/payment/quotation.dart index a36bacc..7141de9 100644 --- a/frontend/pshared/lib/provider/payment/quotation.dart +++ b/frontend/pshared/lib/provider/payment/quotation.dart @@ -8,18 +8,22 @@ 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'; @@ -36,12 +40,17 @@ class QuotationProvider extends ChangeNotifier { PaymentAmountProvider payment, WalletsProvider wallets, PaymentFlowProvider flow, + RecipientsProvider recipients, PaymentMethodsProvider methods, ) { _organizations = venue; 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( @@ -61,6 +70,7 @@ class QuotationProvider extends ChangeNotifier { side: FxSide.sellBaseBuyQuote, ), settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource, + customer: customer, )); } } @@ -73,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/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/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 index b8e0a27..a72b6f8 100644 --- a/frontend/pshared/lib/service/payment/service.dart +++ b/frontend/pshared/lib/service/payment/service.dart @@ -1,5 +1,4 @@ import 'package:logging/logging.dart'; - import 'package:uuid/uuid.dart'; import 'package:pshared/api/requests/payment/initiate.dart'; @@ -27,10 +26,10 @@ class PaymentService { metadata: metadata, ); final response = await AuthorizationService.getPOSTResponse( - _objectType, - '/by-quote/$organizationRef', + _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..2c6c34d 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -13,15 +13,18 @@ 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'; +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'; import 'package:pweb/providers/carousel.dart'; -import 'package:pweb/providers/mock_payment.dart'; import 'package:pweb/providers/operatioins.dart'; import 'package:pweb/providers/two_factor.dart'; import 'package:pweb/providers/upload_history.dart'; @@ -89,16 +92,31 @@ void main() async { ChangeNotifierProvider( create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(), ), - ChangeNotifierProvider( - create: (_) => MockPaymentProvider(), + ChangeNotifierProxyProvider2( + create: (_) => PaymentFlowProvider(initialType: PaymentType.bankAccount), + update: (context, recipients, methods, provider) => provider!..update( + recipients, + methods, + ), ), - ChangeNotifierProvider( create: (_) => OperationProvider(OperationService())..loadOperations(), ), ChangeNotifierProvider( create: (_) => PaymentAmountProvider(), ), + ChangeNotifierProxyProvider6( + create: (_) => QuotationProvider(), + update: (_, organization, payment, wallet, flow, recipients, methods, provider) => + provider!..update(organization, payment, wallet, flow, recipients, methods), + ), + 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 new file mode 100644 index 0000000..7c23eaa --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/widget.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/dashboard/payouts/form.dart'; + + +class PaymentFromWrappingWidget extends StatelessWidget { + const PaymentFromWrappingWidget({super.key}); + + @override + 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 2c10623..5598647 100644 --- a/frontend/pweb/lib/pages/payment_methods/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/page.dart @@ -1,24 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; - import 'package:provider/provider.dart'; -import 'package:pshared/models/payment/methods/data.dart'; -import 'package:pshared/models/payment/methods/type.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; -import 'package:pshared/provider/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'; @@ -60,30 +52,23 @@ class _PaymentPageState extends State { } void _initializePaymentPage() { - final methodsProvider = context.read(); - _handleWalletAutoSelection(methodsProvider); + final flowProvider = context.read(); + flowProvider.setPreferredType(widget.initialPaymentType); } void _handleSearchChanged(String query) { context.read().setQuery(query); } - void _handleRecipientSelected(BuildContext context, Recipient recipient) { + void _handleRecipientSelected(Recipient recipient) { final recipientProvider = context.read(); recipientProvider.setCurrentObject(recipient.id); _clearSearchField(); } - void _handleRecipientCleared(BuildContext context) { + void _handleRecipientCleared() { final recipientProvider = context.read(); - final methodsProvider = context.read(); - recipientProvider.setCurrentObject(null); - context.read().reset( - recipient: null, - availableTypes: _availablePaymentTypes(null, methodsProvider), - preferredType: widget.initialPaymentType, - ); _clearSearchField(); } @@ -93,106 +78,42 @@ 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() { + 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 Widget build(BuildContext context) { final methodsProvider = context.watch(); - final recipientProvider = context.watch(); - final recipient = recipientProvider.currentObject; - final availableTypes = _availablePaymentTypes(recipient, methodsProvider); - - 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), - ), - ), - ); - } - - 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); - } - } - - MethodMap _availablePaymentTypes( - Recipient? recipient, - PaymentMethodsProvider methodsProvider, - ) { - if (recipient == null || !methodsProvider.isReady) return {}; - - final methodsForRecipient = methodsProvider.methods.where( - (method) => !method.isArchived && method.recipientRef == recipient.id, + final recipientProvider = context.read(); + final recipient = context.select( + (provider) => provider.currentObject, ); - return { - for (final method in methodsForRecipient) method.type: method.data, - }; - } - - PaymentMethod? _getPaymentMethodForWallet( - Wallet wallet, - PaymentMethodsProvider methodsProvider, - ) { - if (methodsProvider.methods.isEmpty) { - return null; - } - - return methodsProvider.methods.firstWhereOrNull( - (method) => - method.type == PaymentType.wallet && - (method.description?.contains(wallet.walletUserID) ?? false), + return PaymentPageBody( + onBack: widget.onBack, + fallbackDestination: widget.fallbackDestination, + recipient: recipient, + recipientProvider: recipientProvider, + methodsProvider: methodsProvider, + onWalletSelected: context.read().selectWallet, + searchController: _searchController, + searchFocusNode: _searchFocusNode, + onSearchChanged: _handleSearchChanged, + onRecipientSelected: _handleRecipientSelected, + onRecipientCleared: _handleRecipientCleared, + onSend: _handleSendPayment, ); } } 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..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,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:pshared/models/payment/methods/data.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'; @@ -17,7 +17,7 @@ class PaymentPageBody extends StatelessWidget { final Recipient? recipient; final RecipientsProvider recipientProvider; final PaymentMethodsProvider methodsProvider; - final MethodMap availablePaymentTypes; + final ValueChanged onWalletSelected; final PayoutDestination fallbackDestination; final TextEditingController searchController; final FocusNode searchFocusNode; @@ -32,7 +32,7 @@ class PaymentPageBody extends StatelessWidget { required this.recipient, required this.recipientProvider, required this.methodsProvider, - required this.availablePaymentTypes, + required this.onWalletSelected, required this.fallbackDestination, required this.searchController, required this.searchFocusNode, @@ -60,8 +60,7 @@ class PaymentPageBody extends StatelessWidget { onBack: onBack, recipient: recipient, recipientProvider: recipientProvider, - methodsProvider: methodsProvider, - availablePaymentTypes: availablePaymentTypes, + 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 2638330..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,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/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: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'; @@ -26,8 +22,7 @@ class PaymentPageContent extends StatelessWidget { final ValueChanged? onBack; final Recipient? recipient; final RecipientsProvider recipientProvider; - final PaymentMethodsProvider methodsProvider; - final MethodMap availablePaymentTypes; + final ValueChanged onWalletSelected; final PayoutDestination fallbackDestination; final TextEditingController searchController; final FocusNode searchFocusNode; @@ -41,8 +36,7 @@ class PaymentPageContent extends StatelessWidget { required this.onBack, required this.recipient, required this.recipientProvider, - required this.methodsProvider, - required this.availablePaymentTypes, + required this.onWalletSelected, 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( @@ -84,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( @@ -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/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 2352d0d..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,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/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: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'; @@ -26,8 +22,7 @@ class PaymentPageContent extends StatelessWidget { final ValueChanged? onBack; final Recipient? recipient; final RecipientsProvider recipientProvider; - final PaymentMethodsProvider methodsProvider; - final MethodMap availablePaymentTypes; + final ValueChanged onWalletSelected; final PayoutDestination fallbackDestination; final TextEditingController searchController; final FocusNode searchFocusNode; @@ -41,8 +36,7 @@ class PaymentPageContent extends StatelessWidget { required this.onBack, required this.recipient, required this.recipientProvider, - required this.methodsProvider, - required this.availablePaymentTypes, + required this.onWalletSelected, 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( @@ -84,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( @@ -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 +} 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; + } +}