+ quotation provider

This commit is contained in:
Stephan D
2025-12-11 01:13:13 +01:00
parent bdf766075e
commit a4481fb63d
102 changed files with 2242 additions and 246 deletions

View File

@@ -1,7 +1,9 @@
import 'package:flutter/widgets.dart';
import 'package:collection/collection.dart';
import 'package:go_router/go_router.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
@@ -92,8 +94,6 @@ class PayoutRoutes {
return PayoutDestination.recipients;
case addRecipient:
return PayoutDestination.addrecipient;
case payment:
return PayoutDestination.payment;
case settings:
return PayoutDestination.settings;
case reports:

View File

@@ -312,6 +312,16 @@
"paymentTypeIban": "IBAN",
"paymentTypeWallet": "Wallet",
"paymentTypeCryptoAddress": "Crypto address",
"paymentTypeLedger": "Ledger account",
"paymentTypeManagedWallet": "Managed wallet",
"paymentTypeCardToken": "Card token",
"cryptoAddressLabel": "Crypto address",
"enterCryptoAddress": "Enter a crypto address",
"tokenSymbolLabel": "Token symbol",
"tokenSymbolRequiredWhenNetwork": "Token symbol is required when a network or contract address is specified",
"contractAddressLabel": "Contract address (optional)",
"memoLabel": "Destination tag / memo (optional)",
"cardNumber": "Card Number",
"enterCardNumber": "Enter the card number",

View File

@@ -312,6 +312,16 @@
"paymentTypeIban": "IBAN",
"paymentTypeWallet": "Кошелек",
"paymentTypeCryptoAddress": "Крипто-адрес",
"paymentTypeLedger": "Леджер счет",
"paymentTypeManagedWallet": "Управляемый кошелек",
"paymentTypeCardToken": "Токен карты",
"cryptoAddressLabel": "Крипто-адрес",
"enterCryptoAddress": "Введите крипто-адрес",
"tokenSymbolLabel": "Символ токена",
"tokenSymbolRequiredWhenNetwork": "Укажите символ токена, если выбрана сеть или указан адрес контракта",
"contractAddressLabel": "Адрес контракта (необязательно)",
"memoLabel": "Destination tag / memo (необязательно)",
"cardNumber": "Номер карты",
"enterCardNumber": "Введите номер карты",

View File

@@ -12,6 +12,7 @@ import 'package:pshared/provider/locale.dart';
import 'package:pshared/provider/permissions.dart';
import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/payment/quotation.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
@@ -24,7 +25,6 @@ import 'package:pweb/providers/two_factor.dart';
import 'package:pweb/providers/upload_history.dart';
import 'package:pweb/providers/wallets.dart';
import 'package:pweb/providers/wallet_transactions.dart';
// import 'package:pweb/services/amplitude.dart';
import 'package:pweb/services/operations.dart';
import 'package:pweb/services/payments/history.dart';
import 'package:pweb/services/wallet_transactions.dart';
@@ -70,7 +70,6 @@ void main() async {
update: (context, orgnization, provider) => provider!..update(orgnization),
),
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
ChangeNotifierProvider(
create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(),
),
@@ -92,10 +91,13 @@ void main() async {
ChangeNotifierProvider(
create: (_) => MockPaymentProvider(),
),
ChangeNotifierProvider(
create: (_) => OperationProvider(OperationService())..loadOperations(),
),
ChangeNotifierProxyProvider<OrganizationsProvider, QuotationProvider>(
create: (_) => QuotationProvider(),
update: (context, orgnization, provider) => provider!..update(orgnization),
),
],
child: const PayApp(),
),

View File

@@ -14,7 +14,6 @@ import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/address_book/form/view.dart';
// import 'package:pweb/services/amplitude.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/utils/payment/label.dart';
import 'package:pweb/utils/snackbar.dart';
@@ -54,7 +53,9 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
PaymentType.iban => m.ibanData,
PaymentType.wallet => m.walletData,
PaymentType.bankAccount => m.bankAccountData,
PaymentType.cryptoAddress => m.cryptoAddressData,
PaymentType.externalChain => m.cryptoAddressData,
//TODO: support new payment methods
_ => throw UnimplementedError('Payment method ${m.type} is not supported yet'),
};
}
}

View File

@@ -1,9 +1,14 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/asset.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/models/payment/methods/crypto_address.dart';
import 'package:pshared/utils/l10n/chain.dart';
import 'package:pweb/utils/text_field_styles.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class CryptoAddressForm extends StatefulWidget {
final void Function(CryptoAddressPaymentMethod) onChanged;
@@ -22,28 +27,67 @@ class CryptoAddressForm extends StatefulWidget {
}
class _CryptoAddressFormState extends State<CryptoAddressForm> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _addressCtrl;
late TextEditingController _networkCtrl;
late TextEditingController _destinationTagCtrl;
late TextEditingController _tokenCtrl;
late TextEditingController _contractCtrl;
late TextEditingController _memoCtrl;
late ChainNetwork _chain;
@override
void initState() {
super.initState();
_addressCtrl = TextEditingController(text: widget.initialData?.address);
_networkCtrl = TextEditingController(text: widget.initialData?.network);
_destinationTagCtrl = TextEditingController(text: widget.initialData?.destinationTag);
final initial = widget.initialData;
_chain = initial?.asset?.chain ?? ChainNetwork.unspecified;
_addressCtrl = TextEditingController(text: initial?.address ?? '');
_tokenCtrl = TextEditingController(text: initial?.asset?.tokenSymbol ?? '');
_contractCtrl = TextEditingController(text: initial?.asset?.contractAddress ?? '');
_memoCtrl = TextEditingController(text: initial?.memo ?? '');
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
}
void _emit() {
if (_addressCtrl.text.isNotEmpty && _networkCtrl.text.isNotEmpty) {
widget.onChanged(
CryptoAddressPaymentMethod(
address: _addressCtrl.text,
network: _networkCtrl.text,
destinationTag: _destinationTagCtrl.text.isNotEmpty ? _destinationTagCtrl.text : null,
),
);
bool get _hasChainSelection => _chain != ChainNetwork.unspecified;
String? _validateAddress(AppLocalizations l10n, String? value) {
if (value == null || value.trim().isEmpty) return l10n.enterCryptoAddress;
return null;
}
String? _validateToken(AppLocalizations l10n) {
final token = _tokenCtrl.text.trim();
final contract = _contractCtrl.text.trim();
if ((_hasChainSelection || contract.isNotEmpty) && token.isEmpty) {
return l10n.tokenSymbolRequiredWhenNetwork;
}
return null;
}
PaymentAsset? _buildAsset() {
final token = _tokenCtrl.text.trim();
final contract = _contractCtrl.text.trim();
if (token.isEmpty && contract.isEmpty && !_hasChainSelection) return null;
if (token.isEmpty) return null;
return PaymentAsset(
chain: _chain,
tokenSymbol: token,
contractAddress: contract.isNotEmpty ? contract : null,
);
}
void _emitIfValid() {
if (!(_formKey.currentState?.validate() ?? false)) return;
widget.onChanged(
CryptoAddressPaymentMethod(
asset: _buildAsset(),
address: _addressCtrl.text.trim(),
memo: _memoCtrl.text.trim().isNotEmpty ? _memoCtrl.text.trim() : null,
),
);
}
@override
@@ -54,48 +98,88 @@ class _CryptoAddressFormState extends State<CryptoAddressForm> {
if (newData == null && oldData != null) {
_addressCtrl.clear();
_networkCtrl.clear();
_destinationTagCtrl.clear();
_tokenCtrl.clear();
_contractCtrl.clear();
_memoCtrl.clear();
_chain = ChainNetwork.unspecified;
return;
}
if (newData != null && newData != oldData) {
_addressCtrl.text = newData.address;
_networkCtrl.text = newData.network;
_destinationTagCtrl.text = newData.destinationTag ?? '';
_tokenCtrl.text = newData.asset?.tokenSymbol ?? '';
_contractCtrl.text = newData.asset?.contractAddress ?? '';
_memoCtrl.text = newData.memo ?? '';
_chain = newData.asset?.chain ?? ChainNetwork.unspecified;
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(
readOnly: !widget.isEditable,
controller: _addressCtrl,
decoration: getInputDecoration(context, 'Crypto address', widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
onChanged: (_) => _emit(),
validator: (val) => (val?.isEmpty ?? true) ? 'Enter crypto address' : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _networkCtrl,
decoration: getInputDecoration(context, 'Network', widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
onChanged: (_) => _emit(),
validator: (val) => (val?.isEmpty ?? true) ? 'Enter network' : null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _destinationTagCtrl,
decoration: getInputDecoration(context, 'Destination tag / memo (optional)', widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
onChanged: (_) => _emit(),
),
],
final l10n = AppLocalizations.of(context)!;
return Form(
key: _formKey,
onChanged: _emitIfValid,
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 12.0,
children: [
TextFormField(
readOnly: !widget.isEditable,
controller: _addressCtrl,
decoration: getInputDecoration(context, l10n.cryptoAddressLabel, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => _validateAddress(l10n, val),
),
DropdownButtonFormField<ChainNetwork>(
initialValue: _chain,
decoration: getInputDecoration(context, l10n.walletTopUpNetworkLabel, widget.isEditable),
items: ChainNetwork.values
.map((chain) => DropdownMenuItem(
value: chain,
child: Text(chain.localizedName(context)),
))
.toList(),
onChanged: widget.isEditable
? (value) {
if (value == null) return;
setState(() => _chain = value);
_emitIfValid();
}
: null,
),
TextFormField(
readOnly: !widget.isEditable,
controller: _tokenCtrl,
decoration: getInputDecoration(context, l10n.tokenSymbolLabel, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (_) => _validateToken(l10n),
),
TextFormField(
readOnly: !widget.isEditable,
controller: _contractCtrl,
decoration: getInputDecoration(context, l10n.contractAddressLabel, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
),
TextFormField(
readOnly: !widget.isEditable,
controller: _memoCtrl,
decoration: getInputDecoration(context, l10n.memoLabel, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
),
],
),
);
}
@override
void dispose() {
_addressCtrl.dispose();
_tokenCtrl.dispose();
_contractCtrl.dispose();
_memoCtrl.dispose();
super.dispose();
}
}

View File

@@ -52,7 +52,7 @@ class PaymentMethodForm extends StatelessWidget {
initialData: initialData as RussianBankAccountPaymentMethod?,
isEditable: isEditable,
),
PaymentType.cryptoAddress => CryptoAddressForm(
PaymentType.externalChain => CryptoAddressForm(
onChanged: onChanged,
initialData: initialData as CryptoAddressPaymentMethod?,
isEditable: isEditable,

View File

@@ -13,7 +13,10 @@ IconData iconForPaymentType(PaymentType type) {
return Icons.account_balance_wallet;
case PaymentType.card:
return Icons.credit_card;
case PaymentType.cryptoAddress:
case PaymentType.externalChain:
return Icons.currency_bitcoin;
//TODO: define new payment methods
default:
return Icons.question_mark;
}
}

View File

@@ -1,5 +1,8 @@
import 'package:flutter/widgets.dart';
import 'package:pshared/models/payment/methods/card_token.dart';
import 'package:pshared/models/payment/methods/ledger.dart';
import 'package:pshared/models/payment/methods/managed_wallet.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/payment/type.dart';
@@ -12,21 +15,31 @@ String getPaymentTypeLabel(BuildContext context, PaymentType type) {
final l10n = AppLocalizations.of(context)!;
return switch (type) {
PaymentType.card => l10n.paymentTypeCard,
PaymentType.cardToken => l10n.paymentTypeCardToken,
PaymentType.bankAccount => l10n.paymentTypeBankAccount,
PaymentType.iban => l10n.paymentTypeIban,
PaymentType.wallet => l10n.paymentTypeWallet,
PaymentType.cryptoAddress => l10n.paymentTypeCryptoAddress,
PaymentType.managedWallet => l10n.paymentTypeManagedWallet,
PaymentType.externalChain => l10n.paymentTypeCryptoAddress,
PaymentType.ledger => l10n.paymentTypeLedger,
};
}
String? _displayString(PaymentMethod m) => switch (m.type) {
PaymentType.card => maskCardNumber(m.cardData?.pan),
PaymentType.cardToken => m.dataAsOrNull<CardTokenPaymentMethod>()?.maskedPan,
PaymentType.bankAccount => m.bankAccountData?.accountNumber,
PaymentType.iban => m.ibanData?.iban,
PaymentType.wallet => m.walletData?.walletId,
PaymentType.cryptoAddress => m.cryptoAddressData?.address,
PaymentType.managedWallet => () {
final data = m.dataAsOrNull<ManagedWalletPaymentMethod>();
if (data == null) return null;
return data.asset?.tokenSymbol ?? data.managedWalletRef;
}(),
PaymentType.externalChain => m.cryptoAddressData?.address,
PaymentType.ledger => m.dataAsOrNull<LedgerPaymentMethod>()?.ledgerAccountRef,
};
String getPaymentTypeDescription(BuildContext context, PaymentMethod m) {
return _displayString(m) ?? AppLocalizations.of(context)!.notSet;
}
}