migration to address book service

This commit is contained in:
Stephan D
2025-12-05 02:30:49 +01:00
parent f71cc76f64
commit 2754a7aa13
10 changed files with 158 additions and 66 deletions

View File

@@ -1,7 +1,11 @@
import 'package:pshared/models/describable.dart'; import 'package:pshared/models/describable.dart';
import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/crypto_address.dart';
import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/iban.dart';
import 'package:pshared/models/payment/methods/russian_bank.dart';
import 'package:pshared/models/payment/methods/wallet.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/permissions/bound.dart'; import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/permissions/bound/storable.dart'; import 'package:pshared/models/permissions/bound/storable.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
@@ -28,6 +32,19 @@ class PaymentMethod implements PermissionBoundStorable, Describable {
PaymentType get type => data.type; PaymentType get type => data.type;
T dataAs<T extends PaymentMethodData>() {
final currentData = data;
if (currentData is T) return currentData;
throw StateError('Payment method data is ${currentData.runtimeType}, requested $T for type $type');
}
T? dataAsOrNull<T extends PaymentMethodData>() => data is T ? data as T : null;
CardPaymentMethod? get cardData => dataAsOrNull<CardPaymentMethod>();
IbanPaymentMethod? get ibanData => dataAsOrNull<IbanPaymentMethod>();
RussianBankAccountPaymentMethod? get bankAccountData => dataAsOrNull<RussianBankAccountPaymentMethod>();
WalletPaymentMethod? get walletData => dataAsOrNull<WalletPaymentMethod>();
CryptoAddressPaymentMethod? get cryptoAddressData => dataAsOrNull<CryptoAddressPaymentMethod>();
@override @override
String get id => storable.id; String get id => storable.id;
@override @override

View File

@@ -10,17 +10,19 @@ import 'package:pshared/service/recipient/pmethods.dart';
class PaymentMethodsProvider extends GenericProvider<PaymentMethod> { class PaymentMethodsProvider extends GenericProvider<PaymentMethod> {
late OrganizationsProvider _organizations; late OrganizationsProvider _organizations;
late RecipientsProvider _recipients;
PaymentMethodsProvider() : super(service: PaymentMethodService.basicService); PaymentMethodsProvider() : super(service: PaymentMethodService.basicService);
List<PaymentMethod> get methods => List<PaymentMethod>.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt))); List<PaymentMethod> get methods => List<PaymentMethod>.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt)));
void updateProviders(OrganizationsProvider organizations, RecipientsProvider recipients) { void updateProviders(OrganizationsProvider organizations, RecipientsProvider recipients) {
if (recipients.currentObject != null) loadMethods(organizations, recipients.currentObject?.id);
}
Future<void> loadMethods(OrganizationsProvider organizations, String? recipientRef) async {
_organizations = organizations; _organizations = organizations;
_recipients = recipients; if (_organizations.isOrganizationSet && (recipientRef != null)) {
if (_organizations.isOrganizationSet && (_recipients.currentObject != null)) { return load(_organizations.current.id, recipientRef);
load(_organizations.current.id, _recipients.currentObject!.id);
} }
} }

View File

@@ -2,16 +2,12 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/crypto_address.dart';
import 'package:pshared/models/payment/methods/iban.dart';
import 'package:pshared/models/payment/methods/russian_bank.dart';
import 'package:pshared/models/payment/methods/wallet.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/models/recipient/status.dart'; import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/models/recipient/type.dart'; import 'package:pshared/models/recipient/type.dart';
import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pweb/pages/address_book/form/view.dart'; import 'package:pweb/pages/address_book/form/view.dart';
import 'package:pweb/services/amplitude.dart'; import 'package:pweb/services/amplitude.dart';
@@ -36,6 +32,25 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
RecipientType _type = RecipientType.internal; RecipientType _type = RecipientType.internal;
RecipientStatus _status = RecipientStatus.ready; RecipientStatus _status = RecipientStatus.ready;
final Map<PaymentType, Object?> _methods = {}; final Map<PaymentType, Object?> _methods = {};
late PaymentMethodsProvider _methodsProvider;
Future<void> _loadMethods() async {
_methodsProvider = PaymentMethodsProvider()..addListener(_onProviderChanged);
await _methodsProvider.loadMethods(
context.read<OrganizationsProvider>(),
widget.recipient?.id,
);
for (final m in _methodsProvider.methods) {
_methods[m.type] = switch (m.type) {
PaymentType.card => m.cardData,
PaymentType.iban => m.ibanData,
PaymentType.wallet => m.walletData,
PaymentType.bankAccount => m.bankAccountData,
PaymentType.cryptoAddress => m.cryptoAddressData,
};
}
}
@override @override
void initState() { void initState() {
@@ -45,12 +60,7 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
_emailCtrl = TextEditingController(text: r?.email ?? ''); _emailCtrl = TextEditingController(text: r?.email ?? '');
_type = r?.type ?? RecipientType.internal; _type = r?.type ?? RecipientType.internal;
_status = r?.status ?? RecipientStatus.ready; _status = r?.status ?? RecipientStatus.ready;
_loadMethods();
if (r?.card != null) _methods[PaymentType.card] = r!.card;
if (r?.iban != null) _methods[PaymentType.iban] = r!.iban;
if (r?.wallet != null) _methods[PaymentType.wallet] = r!.wallet;
if (r?.bank != null) _methods[PaymentType.bankAccount] = r!.bank;
if (r?.cryptoAddress != null) _methods[PaymentType.cryptoAddress] = r!.cryptoAddress;
} }
//TODO: Change when registration is ready //TODO: Change when registration is ready
@@ -81,6 +91,15 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
widget.onSaved?.call(recipient); widget.onSaved?.call(recipient);
} }
@override
void dispose() {
_methodsProvider.removeListener(_onProviderChanged);
_methodsProvider.dispose();
super.dispose();
}
void _onProviderChanged() => setState(() {});
@override @override
Widget build(BuildContext context) => FormView( Widget build(BuildContext context) => FormView(
formKey: _formKey, formKey: _formKey,

View File

@@ -4,12 +4,12 @@ import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/recipient/filter.dart'; import 'package:pshared/models/recipient/filter.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/address_book/page/filter_button.dart'; import 'package:pweb/pages/address_book/page/filter_button.dart';
import 'package:pweb/pages/address_book/page/header.dart'; import 'package:pweb/pages/address_book/page/header.dart';
import 'package:pweb/pages/address_book/page/list.dart'; import 'package:pweb/pages/address_book/page/list.dart';
import 'package:pweb/pages/address_book/page/search.dart'; import 'package:pweb/pages/address_book/page/search.dart';
import 'package:pweb/providers/recipient.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -42,7 +42,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final provider = context.read<RecipientProvider>(); final provider = context.read<RecipientsProvider>();
_searchController = TextEditingController(text: provider.query); _searchController = TextEditingController(text: provider.query);
_searchFocusNode = FocusNode(); _searchFocusNode = FocusNode();
} }
@@ -54,7 +54,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
super.dispose(); super.dispose();
} }
void _syncSearchField(RecipientProvider provider) { void _syncSearchField(RecipientsProvider provider) {
final query = provider.query; final query = provider.query;
if (_searchController.text == query) return; if (_searchController.text == query) return;
@@ -68,7 +68,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
final provider = context.watch<RecipientProvider>(); final provider = context.watch<RecipientsProvider>();
_syncSearchField(provider); _syncSearchField(provider);
if (provider.isLoading) { if (provider.isLoading) {

View File

@@ -1,12 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart'; import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pweb/pages/address_book/page/recipient/info_row.dart'; import 'package:pweb/pages/address_book/page/recipient/info_row.dart';
import 'package:pweb/utils/payment/label.dart';
class RecipientPaymentRow extends StatelessWidget { class RecipientPaymentRow extends StatefulWidget {
final Recipient recipient; final Recipient recipient;
final double spacing; final double spacing;
@@ -16,37 +20,43 @@ class RecipientPaymentRow extends StatelessWidget {
this.spacing = 18 this.spacing = 18
}); });
@override
State<RecipientPaymentRow> createState() => _RecipientPaymentRowState();
}
class _RecipientPaymentRowState extends State<RecipientPaymentRow> {
late final PaymentMethodsProvider _methodsProvider;
@override
void initState() {
super.initState();
_methodsProvider = PaymentMethodsProvider()
..addListener(_onProviderChanged)
..loadMethods(
context.read<OrganizationsProvider>(),
widget.recipient.id,
);
}
@override
void dispose() {
_methodsProvider.removeListener(_onProviderChanged);
_methodsProvider.dispose();
super.dispose();
}
void _onProviderChanged() => setState(() {});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator());
return Row( return Row(
spacing: spacing, spacing: widget.spacing,
children: [ children: _methodsProvider.methods.map((m) => RecipientAddressBookInfoRow(
if (recipient.bank?.accountNumber.isNotEmpty ?? false) type: m.type,
RecipientAddressBookInfoRow( value: getPaymentTypeDescription(context, m),
type: PaymentType.bankAccount, )).toList(),
value: recipient.bank!.accountNumber
),
if (recipient.card?.pan.isNotEmpty ?? false)
RecipientAddressBookInfoRow(
type: PaymentType.card,
value: recipient.card!.pan
),
if (recipient.iban?.iban.isNotEmpty ?? false)
RecipientAddressBookInfoRow(
type: PaymentType.iban,
value: recipient.iban!.iban
),
if (recipient.wallet?.walletId.isNotEmpty ?? false)
RecipientAddressBookInfoRow(
type: PaymentType.wallet,
value: recipient.wallet!.walletId
),
if (recipient.cryptoAddress?.address.isNotEmpty ?? false)
RecipientAddressBookInfoRow(
type: PaymentType.cryptoAddress,
value: recipient.cryptoAddress!.address,
),
],
); );
} }
} }

View File

@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart'; import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart'; import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart';
@@ -37,13 +36,24 @@ class _RecipientItemState extends State<RecipientItem> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_methodsProvider = PaymentMethodsProvider(); _methodsProvider = PaymentMethodsProvider()
_methodsProvider.updateProviders( ..addListener(_onProviderChanged)
context.read<OrganizationsProvider>(), ..loadMethods(
context.read<RecipientsProvider>(), context.read<OrganizationsProvider>(),
); widget.recipient.id,
);
} }
@override
void dispose() {
_methodsProvider.removeListener(_onProviderChanged);
_methodsProvider.dispose();
super.dispose();
}
void _onProviderChanged() => setState(() {});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator()); if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator());
@@ -78,7 +88,7 @@ class _RecipientItemState extends State<RecipientItem> {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: _methodsProvider.methods.map((m) => PaymentInfoRow( children: _methodsProvider.methods.map((m) => PaymentInfoRow(
label: getPaymentTypeLabel(context, m.type), label: getPaymentTypeLabel(context, m.type),
value: _displayString(m), value: getPaymentTypeDescription(context, m),
)).toList(), )).toList(),
), ),
], ],

View File

@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.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/providers/payment_flow_provider.dart'; import 'package:pweb/providers/payment_flow_provider.dart';
@@ -48,8 +47,6 @@ class _PaymentPageState extends State<PaymentPage> {
void _initializePaymentPage() { void _initializePaymentPage() {
final pageSelector = context.read<PageSelectorProvider>(); final pageSelector = context.read<PageSelectorProvider>();
final methodsProvider = context.read<PaymentMethodsProvider>();
final recipientProvider = context.read<RecipientsProvider>();
pageSelector.handleWalletAutoSelection(); pageSelector.handleWalletAutoSelection();

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:pshared/models/payment/methods/type.dart'; import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/payment/type.dart';
@@ -16,8 +15,6 @@ import 'package:pweb/widgets/sidebar/destinations.dart';
class PageSelectorProvider extends ChangeNotifier { class PageSelectorProvider extends ChangeNotifier {
static final _logger = Logger('provider.page_selector');
PayoutDestination _selected = PayoutDestination.dashboard; PayoutDestination _selected = PayoutDestination.dashboard;
PaymentType? _type; PaymentType? _type;
bool _cameFromRecipientList = false; bool _cameFromRecipientList = false;
@@ -65,7 +62,7 @@ class PageSelectorProvider extends ChangeNotifier {
void goToAddRecipient() { void goToAddRecipient() {
AmplitudeService.recipientAddStarted(); AmplitudeService.recipientAddStarted();
recipientProvider!.setCurrentObject(null); recipientProvider.setCurrentObject(null);
_selected = PayoutDestination.addrecipient; _selected = PayoutDestination.addrecipient;
_cameFromRecipientList = false; _cameFromRecipientList = false;
notifyListeners(); notifyListeners();
@@ -114,7 +111,7 @@ class PageSelectorProvider extends ChangeNotifier {
return null; return null;
} }
return methodsProvider!.methods.firstWhereOrNull( return methodsProvider.methods.firstWhereOrNull(
(method) => method.type == PaymentType.wallet && (method) => method.type == PaymentType.wallet &&
(method.description?.contains(wallet.walletUserID) ?? false), (method.description?.contains(wallet.walletUserID) ?? false),
); );

View File

@@ -1,7 +1,10 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.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:pweb/utils/payment/masking.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -15,3 +18,15 @@ String getPaymentTypeLabel(BuildContext context, PaymentType type) {
PaymentType.cryptoAddress => l10n.paymentTypeCryptoAddress, PaymentType.cryptoAddress => l10n.paymentTypeCryptoAddress,
}; };
} }
String? _displayString(PaymentMethod m) => switch (m.type) {
PaymentType.card => maskCardNumber(m.cardData?.pan),
PaymentType.bankAccount => m.bankAccountData?.accountNumber,
PaymentType.iban => m.ibanData?.iban,
PaymentType.wallet => m.walletData?.walletId,
PaymentType.cryptoAddress => m.cryptoAddressData?.address,
};
String getPaymentTypeDescription(BuildContext context, PaymentMethod m) {
return _displayString(m) ?? AppLocalizations.of(context)!.notSet;
}

View File

@@ -0,0 +1,25 @@
import 'dart:math';
/// Masks a card number, leaving only the last 4 digits visible.
/// Returns 'N/A' when the input is null or empty.
String? maskCardNumber(String? pan) {
if (pan == null || pan.isEmpty) return null;
// Strip non-digits to avoid leaking formatting patterns.
final digits = pan.replaceAll(RegExp(r'\D'), '');
if (digits.length <= 4) return digits;
final last4 = digits.substring(digits.length - 4);
final maskedPrefix = '*' * (digits.length - 4);
final masked = '$maskedPrefix$last4';
// Group into blocks of 4 for readability.
final groups = <String>[];
for (var i = 0; i < masked.length; i += 4) {
final end = min(i + 4, masked.length);
groups.add(masked.substring(i, end));
}
return groups.join(' ');
}