Fixed search field in payment page and cleaned up payment flow #187

Closed
protuberanets wants to merge 5 commits from SEND018 into main
9 changed files with 91 additions and 79 deletions
Showing only changes of commit f339630115 - Show all commits

View File

@@ -1,6 +1,8 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:pshared/models/payment/methods/data.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/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/provider.dart';
@@ -11,7 +13,7 @@ class PaymentFlowProvider extends ChangeNotifier {
PaymentType _selectedType; PaymentType _selectedType;
PaymentType? _preferredType; PaymentType? _preferredType;
PaymentMethodData? _manualPaymentData; PaymentMethodData? _manualPaymentData;
MethodMap _availableTypes = {}; List<PaymentMethod> _recipientMethods = [];
Recipient? _recipient; Recipient? _recipient;
PaymentFlowProvider({ PaymentFlowProvider({
@@ -23,15 +25,22 @@ class PaymentFlowProvider extends ChangeNotifier {
PaymentType get selectedType => _selectedType; PaymentType get selectedType => _selectedType;
PaymentMethodData? get manualPaymentData => _manualPaymentData; PaymentMethodData? get manualPaymentData => _manualPaymentData;
Recipient? get recipient => _recipient; Recipient? get recipient => _recipient;
PaymentMethod? get selectedMethod => hasRecipient
? _recipientMethods.firstWhereOrNull((method) => method.type == _selectedType)
: null;
bool get hasRecipient => _recipient != null; bool get hasRecipient => _recipient != null;
MethodMap get availableTypes => hasRecipient MethodMap get availableTypes => hasRecipient
? _availableTypes ? _buildAvailableTypes(_recipientMethods)
: {for (final type in PaymentType.values) type: null}; : {for (final type in PaymentType.values) type: null};
PaymentMethodData? get selectedPaymentData => PaymentMethodData? get selectedPaymentData =>
hasRecipient ? _availableTypes[_selectedType] : _manualPaymentData; hasRecipient ? selectedMethod?.data : _manualPaymentData;
List<PaymentMethod> get methodsForRecipient => hasRecipient
? List<PaymentMethod>.unmodifiable(_recipientMethods)
: const [];
void update( void update(
RecipientsProvider recipientsProvider, RecipientsProvider recipientsProvider,
@@ -39,12 +48,16 @@ class PaymentFlowProvider extends ChangeNotifier {
) => ) =>
_applyState( _applyState(
recipient: recipientsProvider.currentObject, recipient: recipientsProvider.currentObject,
availableTypes: methodsProvider.availableTypesForRecipient(recipientsProvider.currentObject), methods: methodsProvider.methodsForRecipient(recipientsProvider.currentObject),
preferredType: _preferredType, preferredType: _preferredType,
forceResetManualData: false, forceResetManualData: false,
); );
void selectType(PaymentType type, {bool resetManualData = false}) { void selectType(PaymentType type, {bool resetManualData = false}) {
if (hasRecipient && !availableTypes.containsKey(type)) {
return;
}
if (_selectedType == type && (!resetManualData || _manualPaymentData == null)) { if (_selectedType == type && (!resetManualData || _manualPaymentData == null)) {
return; return;
} }
@@ -69,7 +82,7 @@ class PaymentFlowProvider extends ChangeNotifier {
_preferredType = preferredType; _preferredType = preferredType;
_applyState( _applyState(
recipient: _recipient, recipient: _recipient,
availableTypes: _availableTypes, methods: _recipientMethods,
preferredType: _preferredType, preferredType: _preferredType,
forceResetManualData: false, forceResetManualData: false,
); );
@@ -101,10 +114,11 @@ class PaymentFlowProvider extends ChangeNotifier {
void _applyState({ void _applyState({
required Recipient? recipient, required Recipient? recipient,
required MethodMap availableTypes, required List<PaymentMethod> methods,
required PaymentType? preferredType, required PaymentType? preferredType,
required bool forceResetManualData, required bool forceResetManualData,
}) { }) {
final availableTypes = _buildAvailableTypes(methods);
final resolvedType = _resolveSelectedType( final resolvedType = _resolveSelectedType(
recipient: recipient, recipient: recipient,
availableTypes: availableTypes, availableTypes: availableTypes,
@@ -118,8 +132,8 @@ class PaymentFlowProvider extends ChangeNotifier {
hasChanges = true; hasChanges = true;
} }
if (!mapEquals(_availableTypes, availableTypes)) { if (!_hasSameMethods(methods)) {
_availableTypes = availableTypes; _recipientMethods = methods;
hasChanges = true; hasChanges = true;
} }
@@ -135,4 +149,18 @@ class PaymentFlowProvider extends ChangeNotifier {
if (hasChanges) notifyListeners(); if (hasChanges) notifyListeners();
} }
MethodMap _buildAvailableTypes(List<PaymentMethod> methods) => {
for (final method in methods) method.type: method.data,
};
bool _hasSameMethods(List<PaymentMethod> 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;
}
} }

View File

@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/models/payment/wallet.dart';
@@ -38,10 +39,7 @@ class WalletsProvider with ChangeNotifier {
throw Exception('update wallet is not implemented'); throw Exception('update wallet is not implemented');
} }
void selectWallet(Wallet wallet) { void selectWallet(Wallet wallet) => _setSelectedWallet(wallet);
_selectedWallet = wallet;
notifyListeners();
}
Future<void> loadWalletsWithBalances() async { Future<void> loadWalletsWithBalances() async {
_setResource(_resource.copyWith(isLoading: true, error: null)); _setResource(_resource.copyWith(isLoading: true, error: null));
@@ -98,6 +96,25 @@ class WalletsProvider with ChangeNotifier {
void _setResource(Resource<List<Wallet>> newResource) { void _setResource(Resource<List<Wallet>> newResource) {
_resource = newResource; _resource = newResource;
_selectedWallet = _resolveSelectedWallet(_selectedWallet, wallets);
notifyListeners();
}
Wallet? _resolveSelectedWallet(Wallet? current, List<Wallet> 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(); notifyListeners();
} }
} }

View File

@@ -19,8 +19,8 @@ import 'package:pshared/provider/payment/quotation.dart';
import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/provider.dart';
import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/payment/wallets.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/models/payment/type.dart';
import 'package:pshared/service/payment/wallets.dart';
import 'package:pweb/app/app.dart'; import 'package:pweb/app/app.dart';
import 'package:pweb/app/timeago.dart'; import 'package:pweb/app/timeago.dart';

View File

@@ -1,17 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:provider/provider.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/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/payment/flow.dart'; import 'package:pshared/provider/payment/flow.dart';
import 'package:pshared/provider/payment/provider.dart'; import 'package:pshared/provider/payment/provider.dart';
import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/provider.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/provider/payment/wallets.dart'; import 'package:pshared/provider/payment/wallets.dart';
import 'package:pweb/pages/payment_methods/payment_page/body.dart'; import 'package:pweb/pages/payment_methods/payment_page/body.dart';
@@ -57,9 +53,7 @@ class _PaymentPageState extends State<PaymentPage> {
void _initializePaymentPage() { void _initializePaymentPage() {
final flowProvider = context.read<PaymentFlowProvider>(); final flowProvider = context.read<PaymentFlowProvider>();
final methodsProvider = context.read<PaymentMethodsProvider>();
flowProvider.setPreferredType(widget.initialPaymentType); flowProvider.setPreferredType(widget.initialPaymentType);
_handleWalletAutoSelection(methodsProvider);
} }
void _handleSearchChanged(String query) { void _handleSearchChanged(String query) {
@@ -113,6 +107,7 @@ class _PaymentPageState extends State<PaymentPage> {
recipient: recipient, recipient: recipient,
recipientProvider: recipientProvider, recipientProvider: recipientProvider,
methodsProvider: methodsProvider, methodsProvider: methodsProvider,
onWalletSelected: context.read<WalletsProvider>().selectWallet,
searchController: _searchController, searchController: _searchController,
searchFocusNode: _searchFocusNode, searchFocusNode: _searchFocusNode,
onSearchChanged: _handleSearchChanged, onSearchChanged: _handleSearchChanged,
@@ -121,29 +116,4 @@ class _PaymentPageState extends State<PaymentPage> {
onSend: _handleSendPayment, onSend: _handleSendPayment,
); );
} }
void _handleWalletAutoSelection(PaymentMethodsProvider methodsProvider) {
final wallet = context.read<WalletsProvider>().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),
);
}
} }

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.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/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/provider.dart';
@@ -16,6 +17,7 @@ class PaymentPageBody extends StatelessWidget {
final Recipient? recipient; final Recipient? recipient;
final RecipientsProvider recipientProvider; final RecipientsProvider recipientProvider;
final PaymentMethodsProvider methodsProvider; final PaymentMethodsProvider methodsProvider;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination; final PayoutDestination fallbackDestination;
final TextEditingController searchController; final TextEditingController searchController;
final FocusNode searchFocusNode; final FocusNode searchFocusNode;
@@ -30,6 +32,7 @@ class PaymentPageBody extends StatelessWidget {
required this.recipient, required this.recipient,
required this.recipientProvider, required this.recipientProvider,
required this.methodsProvider, required this.methodsProvider,
required this.onWalletSelected,
required this.fallbackDestination, required this.fallbackDestination,
required this.searchController, required this.searchController,
required this.searchFocusNode, required this.searchFocusNode,
@@ -57,7 +60,7 @@ class PaymentPageBody extends StatelessWidget {
onBack: onBack, onBack: onBack,
recipient: recipient, recipient: recipient,
recipientProvider: recipientProvider, recipientProvider: recipientProvider,
methodsProvider: methodsProvider, onWalletSelected: onWalletSelected,
fallbackDestination: fallbackDestination, fallbackDestination: fallbackDestination,
searchController: searchController, searchController: searchController,
searchFocusNode: searchFocusNode, searchFocusNode: searchFocusNode,

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/recipient/recipient.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/recipient/provider.dart';
import 'package:pweb/pages/payment_methods/payment_page/back_button.dart'; import 'package:pweb/pages/payment_methods/payment_page/back_button.dart';
@@ -22,7 +22,7 @@ class PaymentPageContent extends StatelessWidget {
final ValueChanged<Recipient?>? onBack; final ValueChanged<Recipient?>? onBack;
final Recipient? recipient; final Recipient? recipient;
final RecipientsProvider recipientProvider; final RecipientsProvider recipientProvider;
final PaymentMethodsProvider methodsProvider; final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination; final PayoutDestination fallbackDestination;
final TextEditingController searchController; final TextEditingController searchController;
final FocusNode searchFocusNode; final FocusNode searchFocusNode;
@@ -36,7 +36,7 @@ class PaymentPageContent extends StatelessWidget {
required this.onBack, required this.onBack,
required this.recipient, required this.recipient,
required this.recipientProvider, required this.recipientProvider,
required this.methodsProvider, required this.onWalletSelected,
required this.fallbackDestination, required this.fallbackDestination,
required this.searchController, required this.searchController,
required this.searchFocusNode, required this.searchFocusNode,
@@ -77,7 +77,7 @@ class PaymentPageContent extends StatelessWidget {
SectionTitle(loc.sourceOfFunds), SectionTitle(loc.sourceOfFunds),
SizedBox(height: dimensions.paddingSmall), SizedBox(height: dimensions.paddingSmall),
PaymentMethodSelector( PaymentMethodSelector(
onMethodChanged: (m) => methodsProvider.setCurrentObject(m.id), onMethodChanged: onWalletSelected,
), ),
SizedBox(height: dimensions.paddingXLarge), SizedBox(height: dimensions.paddingXLarge),
RecipientSection( RecipientSection(

View File

@@ -17,9 +17,11 @@ class PaymentMethodSelector extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) => Consumer<WalletsProvider>(builder:(context, provider, _) => PaymentMethodDropdown( Widget build(BuildContext context) => Consumer<WalletsProvider>(
methods: provider.wallets, builder: (context, provider, _) => PaymentMethodDropdown(
initialValue: provider.selectedWallet, methods: provider.wallets,
onChanged: onMethodChanged, selectedMethod: provider.selectedWallet,
)); onChanged: onMethodChanged,
),
);
} }

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/recipient/recipient.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/recipient/provider.dart';
import 'package:pweb/pages/dashboard/payouts/widget.dart'; import 'package:pweb/pages/dashboard/payouts/widget.dart';
@@ -22,7 +22,7 @@ class PaymentPageContent extends StatelessWidget {
final ValueChanged<Recipient?>? onBack; final ValueChanged<Recipient?>? onBack;
final Recipient? recipient; final Recipient? recipient;
final RecipientsProvider recipientProvider; final RecipientsProvider recipientProvider;
final PaymentMethodsProvider methodsProvider; final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination; final PayoutDestination fallbackDestination;
final TextEditingController searchController; final TextEditingController searchController;
final FocusNode searchFocusNode; final FocusNode searchFocusNode;
@@ -36,7 +36,7 @@ class PaymentPageContent extends StatelessWidget {
required this.onBack, required this.onBack,
required this.recipient, required this.recipient,
required this.recipientProvider, required this.recipientProvider,
required this.methodsProvider, required this.onWalletSelected,
required this.fallbackDestination, required this.fallbackDestination,
required this.searchController, required this.searchController,
required this.searchFocusNode, required this.searchFocusNode,
@@ -77,7 +77,7 @@ class PaymentPageContent extends StatelessWidget {
SectionTitle(loc.sourceOfFunds), SectionTitle(loc.sourceOfFunds),
SizedBox(height: dimensions.paddingSmall), SizedBox(height: dimensions.paddingSmall),
PaymentMethodSelector( PaymentMethodSelector(
onMethodChanged: (m) => methodsProvider.setCurrentObject(m.id), onMethodChanged: onWalletSelected,
), ),
SizedBox(height: dimensions.paddingXLarge), SizedBox(height: dimensions.paddingXLarge),
RecipientSection( RecipientSection(

View File

@@ -8,40 +8,27 @@ import 'package:pweb/pages/payment_methods/icon.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentMethodDropdown extends StatefulWidget { class PaymentMethodDropdown extends StatelessWidget {
final List<Wallet> methods; final List<Wallet> methods;
final ValueChanged<Wallet> onChanged; final ValueChanged<Wallet> onChanged;
final Wallet? initialValue; final Wallet? selectedMethod;
const PaymentMethodDropdown({ const PaymentMethodDropdown({
super.key, super.key,
required this.methods, required this.methods,
required this.onChanged, required this.onChanged,
this.initialValue, this.selectedMethod,
}); });
@override
State<PaymentMethodDropdown> createState() => _PaymentMethodDropdownState();
}
class _PaymentMethodDropdownState extends State<PaymentMethodDropdown> {
late Wallet _selectedMethod;
@override
void initState() {
super.initState();
_selectedMethod = widget.initialValue ?? widget.methods.first;
}
@override @override
Widget build(BuildContext context) => DropdownButtonFormField<Wallet>( Widget build(BuildContext context) => DropdownButtonFormField<Wallet>(
dropdownColor: Theme.of(context).colorScheme.onSecondary, dropdownColor: Theme.of(context).colorScheme.onSecondary,
initialValue: _selectedMethod, value: _getSelectedMethod(),
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.whereGetMoney, labelText: AppLocalizations.of(context)!.whereGetMoney,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
), ),
items: widget.methods.map((method) => DropdownMenuItem<Wallet>( items: methods.map((method) => DropdownMenuItem<Wallet>(
value: method, value: method,
child: Row( child: Row(
children: [ children: [
@@ -53,9 +40,14 @@ class _PaymentMethodDropdownState extends State<PaymentMethodDropdown> {
)).toList(), )).toList(),
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
setState(() => _selectedMethod = value); onChanged(value);
widget.onChanged(value);
} }
}, },
); );
Wallet? _getSelectedMethod() {
if (selectedMethod != null) return selectedMethod;
if (methods.isEmpty) return null;
return methods.first;
}
} }