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 +}