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/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/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/storable.dart';
import 'package:pshared/models/storable.dart';
@@ -28,6 +32,19 @@ class PaymentMethod implements PermissionBoundStorable, Describable {
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
String get id => storable.id;
@override

View File

@@ -10,17 +10,19 @@ import 'package:pshared/service/recipient/pmethods.dart';
class PaymentMethodsProvider extends GenericProvider<PaymentMethod> {
late OrganizationsProvider _organizations;
late RecipientsProvider _recipients;
PaymentMethodsProvider() : super(service: PaymentMethodService.basicService);
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) {
if (recipients.currentObject != null) loadMethods(organizations, recipients.currentObject?.id);
}
Future<void> loadMethods(OrganizationsProvider organizations, String? recipientRef) async {
_organizations = organizations;
_recipients = recipients;
if (_organizations.isOrganizationSet && (_recipients.currentObject != null)) {
load(_organizations.current.id, _recipients.currentObject!.id);
if (_organizations.isOrganizationSet && (recipientRef != null)) {
return load(_organizations.current.id, recipientRef);
}
}

View File

@@ -2,16 +2,12 @@ import 'package:flutter/material.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/recipient/recipient.dart';
import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/models/recipient/type.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/services/amplitude.dart';
@@ -36,6 +32,25 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
RecipientType _type = RecipientType.internal;
RecipientStatus _status = RecipientStatus.ready;
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
void initState() {
@@ -45,12 +60,7 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
_emailCtrl = TextEditingController(text: r?.email ?? '');
_type = r?.type ?? RecipientType.internal;
_status = r?.status ?? RecipientStatus.ready;
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;
_loadMethods();
}
//TODO: Change when registration is ready
@@ -81,6 +91,15 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
widget.onSaved?.call(recipient);
}
@override
void dispose() {
_methodsProvider.removeListener(_onProviderChanged);
_methodsProvider.dispose();
super.dispose();
}
void _onProviderChanged() => setState(() {});
@override
Widget build(BuildContext context) => FormView(
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/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/header.dart';
import 'package:pweb/pages/address_book/page/list.dart';
import 'package:pweb/pages/address_book/page/search.dart';
import 'package:pweb/providers/recipient.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -42,7 +42,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
@override
void initState() {
super.initState();
final provider = context.read<RecipientProvider>();
final provider = context.read<RecipientsProvider>();
_searchController = TextEditingController(text: provider.query);
_searchFocusNode = FocusNode();
}
@@ -54,7 +54,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
super.dispose();
}
void _syncSearchField(RecipientProvider provider) {
void _syncSearchField(RecipientsProvider provider) {
final query = provider.query;
if (_searchController.text == query) return;
@@ -68,7 +68,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final provider = context.watch<RecipientProvider>();
final provider = context.watch<RecipientsProvider>();
_syncSearchField(provider);
if (provider.isLoading) {

View File

@@ -1,12 +1,16 @@
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/provider/organizations.dart';
import 'package:pshared/provider/recipient/pmethods.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 double spacing;
@@ -16,37 +20,43 @@ class RecipientPaymentRow extends StatelessWidget {
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
Widget build(BuildContext context) {
if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator());
return Row(
spacing: spacing,
children: [
if (recipient.bank?.accountNumber.isNotEmpty ?? false)
RecipientAddressBookInfoRow(
type: PaymentType.bankAccount,
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,
),
],
spacing: widget.spacing,
children: _methodsProvider.methods.map((m) => RecipientAddressBookInfoRow(
type: m.type,
value: getPaymentTypeDescription(context, m),
)).toList(),
);
}
}

View File

@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/organizations.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/long_list/info_row.dart';
@@ -37,13 +36,24 @@ class _RecipientItemState extends State<RecipientItem> {
@override
void initState() {
super.initState();
_methodsProvider = PaymentMethodsProvider();
_methodsProvider.updateProviders(
context.read<OrganizationsProvider>(),
context.read<RecipientsProvider>(),
);
_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
Widget build(BuildContext context) {
if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator());
@@ -78,7 +88,7 @@ class _RecipientItemState extends State<RecipientItem> {
crossAxisAlignment: CrossAxisAlignment.end,
children: _methodsProvider.methods.map((m) => PaymentInfoRow(
label: getPaymentTypeLabel(context, m.type),
value: _displayString(m),
value: getPaymentTypeDescription(context, m),
)).toList(),
),
],

View File

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

View File

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

View File

@@ -1,7 +1,10 @@
import 'package:flutter/widgets.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/utils/payment/masking.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -15,3 +18,15 @@ String getPaymentTypeLabel(BuildContext context, PaymentType type) {
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(' ');
}