From f71cc76f64fb3f31127f05ff501b65677e6bce3e Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 5 Dec 2025 01:32:41 +0100 Subject: [PATCH] temp build --- .../internal/server/internal/serverimp.go | 7 +- .../mntx/internal/service/monetix/client.go | 3 +- api/pkg/model/payment.go | 2 + .../lib/api/responses/payment_method.dart | 21 +++ .../pshared/lib/api/responses/recipient.dart | 19 ++ .../pshared/lib/data/dto/payment/method.dart | 6 + .../lib/data/mapper/payment/method.dart | 12 +- .../lib/data/mapper/recipient/recipient.dart | 6 +- frontend/pshared/lib/l10n/en.arb | 10 ++ frontend/pshared/lib/l10n/ru.arb | 10 ++ .../lib/models/payment/methods/type.dart | 69 +++++-- .../lib/models/payment/payment_method.dart | 46 ----- .../lib/models/recipient/recipient.dart | 137 +++++++------- .../lib/models/recipient/recipient_model.dart | 64 ------- .../lib/provider/recipient/pmethods.dart | 59 ++++++ .../lib/provider/recipient/provider.dart} | 50 ++---- frontend/pshared/lib/provider/template.dart | 78 +++++++- .../lib/service/recipient/pmethods.dart | 37 ++++ .../lib/service/recipient/service.dart | 37 ++++ frontend/pshared/lib/service/template.dart | 48 +++-- frontend/pshared/lib/utils/http/params.dart | 38 ++++ frontend/pshared/lib/widgets/template.dart | 8 +- frontend/pweb/lib/l10n/en.arb | 1 + frontend/pweb/lib/l10n/ru.arb | 1 + frontend/pweb/lib/main.dart | 28 ++- .../lib/pages/address_book/form/page.dart | 67 ++++--- .../adress_book/long_list/info_row.dart | 16 +- .../single/adress_book/long_list/item.dart | 75 ++++---- .../long_list/{long_list.dart => widget.dart} | 0 .../payouts/single/adress_book/widget.dart | 24 +-- .../payment_methods/method_selector.dart | 20 +-- .../pweb/lib/pages/payment_methods/page.dart | 26 +-- .../pweb/lib/pages/payment_methods/title.dart | 23 ++- .../widgets/payment_page_body.dart | 11 +- .../widgets/recipient_section.dart | 4 +- .../pages/payment_methods/widgets/search.dart | 4 +- .../pages/payout_page/methods/controller.dart | 25 +-- .../lib/pages/payout_page/methods/list.dart | 3 +- .../lib/pages/payout_page/methods/widget.dart | 1 - frontend/pweb/lib/pages/payout_page/page.dart | 4 +- .../pweb/lib/providers/page_selector.dart | 113 +++++------- .../pweb/lib/providers/payment_methods.dart | 68 ------- .../pweb/lib/providers/upload_history.dart | 5 +- .../{upload_history.dart => history.dart} | 0 .../pweb/lib/services/payments/methods.dart | 48 +++++ .../services/payments/payment_methods.dart | 48 ----- .../lib/services/recipient/recipient.dart | 170 +++++++++--------- frontend/pweb/lib/utils/payment/dropdown.dart | 2 +- frontend/pweb/lib/utils/payment/label.dart | 2 +- frontend/pweb/lib/widgets/sidebar/page.dart | 4 +- 50 files changed, 853 insertions(+), 707 deletions(-) create mode 100644 frontend/pshared/lib/api/responses/payment_method.dart create mode 100644 frontend/pshared/lib/api/responses/recipient.dart delete mode 100644 frontend/pshared/lib/models/payment/payment_method.dart delete mode 100644 frontend/pshared/lib/models/recipient/recipient_model.dart create mode 100644 frontend/pshared/lib/provider/recipient/pmethods.dart rename frontend/{pweb/lib/providers/recipient.dart => pshared/lib/provider/recipient/provider.dart} (54%) create mode 100644 frontend/pshared/lib/service/recipient/pmethods.dart create mode 100644 frontend/pshared/lib/service/recipient/service.dart create mode 100644 frontend/pshared/lib/utils/http/params.dart rename frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/{long_list.dart => widget.dart} (100%) delete mode 100644 frontend/pweb/lib/providers/payment_methods.dart rename frontend/pweb/lib/services/payments/{upload_history.dart => history.dart} (100%) create mode 100644 frontend/pweb/lib/services/payments/methods.dart delete mode 100644 frontend/pweb/lib/services/payments/payment_methods.dart diff --git a/api/gateway/mntx/internal/server/internal/serverimp.go b/api/gateway/mntx/internal/server/internal/serverimp.go index c3fd661..fc180d0 100644 --- a/api/gateway/mntx/internal/server/internal/serverimp.go +++ b/api/gateway/mntx/internal/server/internal/serverimp.go @@ -13,6 +13,7 @@ import ( "github.com/go-chi/chi/v5" mntxservice "github.com/tech/sendico/gateway/mntx/internal/service/gateway" + "github.com/tech/sendico/gateway/mntx/internal/service/monetix" "github.com/tech/sendico/pkg/api/routers" "github.com/tech/sendico/pkg/merrors" msg "github.com/tech/sendico/pkg/messaging" @@ -168,7 +169,7 @@ func (i *Imp) loadConfig() (*config, error) { return cfg, nil } -func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (mntxservice.MonetixConfig, error) { +func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (monetix.Config, error) { baseURL := strings.TrimSpace(cfg.BaseURL) if env := strings.TrimSpace(cfg.BaseURLEnv); env != "" { if val := strings.TrimSpace(os.Getenv(env)); val != "" { @@ -183,7 +184,7 @@ func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (mntxservice.MonetixConfig if id, err := strconv.ParseInt(raw, 10, 64); err == nil { projectID = id } else { - return mntxservice.MonetixConfig{}, merrors.InvalidArgument("invalid project id in env "+cfg.ProjectIDEnv, "monetix.project_id") + return monetix.Config{}, merrors.InvalidArgument("invalid project id in env "+cfg.ProjectIDEnv, "monetix.project_id") } } } @@ -203,7 +204,7 @@ func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (mntxservice.MonetixConfig statusSuccess := strings.TrimSpace(cfg.StatusSuccess) statusProcessing := strings.TrimSpace(cfg.StatusProcessing) - return mntxservice.MonetixConfig{ + return monetix.Config{ BaseURL: baseURL, ProjectID: projectID, SecretKey: secret, diff --git a/api/gateway/mntx/internal/service/monetix/client.go b/api/gateway/mntx/internal/service/monetix/client.go index 4bf169d..cfe5f3b 100644 --- a/api/gateway/mntx/internal/service/monetix/client.go +++ b/api/gateway/mntx/internal/service/monetix/client.go @@ -8,7 +8,6 @@ import ( "encoding/json" "net/http" - "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "go.uber.org/zap" ) @@ -31,7 +30,7 @@ func NewClient(cfg Config, httpClient *http.Client, logger mlogger.Logger) *Clie return &Client{ cfg: cfg, client: client, - logger: cl.Named("monetix_client"), + logger: cl.Named("client"), } } diff --git a/api/pkg/model/payment.go b/api/pkg/model/payment.go index 14b637d..51be177 100644 --- a/api/pkg/model/payment.go +++ b/api/pkg/model/payment.go @@ -64,8 +64,10 @@ func (t *PaymentType) UnmarshalJSON(data []byte) error { type PaymentMethod struct { PermissionBound `bson:",inline" json:",inline"` + Describable `bson:",inline" json:",inline"` RecipientRef primitive.ObjectID `bson:"recipientRef" json:"recipientRef"` Type PaymentType `bson:"type" json:"type"` Data bson.Raw `bson:"data" json:"data"` + IsMain bool `bson:"isMain" json:"isMain"` } diff --git a/frontend/pshared/lib/api/responses/payment_method.dart b/frontend/pshared/lib/api/responses/payment_method.dart new file mode 100644 index 0000000..c0f5834 --- /dev/null +++ b/frontend/pshared/lib/api/responses/payment_method.dart @@ -0,0 +1,21 @@ +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/method.dart'; + +part 'payment_method.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class PaymentMethodResponse extends BaseAuthorizedResponse { + + @JsonKey(name: 'payment_methods') + final List paymentMethods; + + const PaymentMethodResponse({required super.accessToken, required this.paymentMethods}); + + factory PaymentMethodResponse.fromJson(Map json) => _$PaymentMethodResponseFromJson(json); + @override + Map toJson() => _$PaymentMethodResponseToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/recipient.dart b/frontend/pshared/lib/api/responses/recipient.dart new file mode 100644 index 0000000..aac0807 --- /dev/null +++ b/frontend/pshared/lib/api/responses/recipient.dart @@ -0,0 +1,19 @@ +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/recipient/recipient.dart'; + +part 'recipient.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class RecipientResponse extends BaseAuthorizedResponse { + final List recipients; + + const RecipientResponse({required super.accessToken, required this.recipients}); + + factory RecipientResponse.fromJson(Map json) => _$RecipientResponseFromJson(json); + @override + Map toJson() => _$RecipientResponseToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/dto/payment/method.dart b/frontend/pshared/lib/data/dto/payment/method.dart index dce7bcd..dc2695e 100644 --- a/frontend/pshared/lib/data/dto/payment/method.dart +++ b/frontend/pshared/lib/data/dto/payment/method.dart @@ -10,6 +10,9 @@ part 'method.g.dart'; class PaymentMethodDTO extends PermissionBoundDTO { final String recipientRef; final String type; + final String name; + final String? description; + final bool isMain; final Map data; @JsonKey(defaultValue: false) @@ -24,6 +27,9 @@ class PaymentMethodDTO extends PermissionBoundDTO { required this.recipientRef, required this.type, required this.data, + required this.name, + required this.isMain, + this.description, this.isArchived = false, }); diff --git a/frontend/pshared/lib/data/mapper/payment/method.dart b/frontend/pshared/lib/data/mapper/payment/method.dart index 7879712..fbd188f 100644 --- a/frontend/pshared/lib/data/mapper/payment/method.dart +++ b/frontend/pshared/lib/data/mapper/payment/method.dart @@ -10,20 +10,21 @@ import 'package:pshared/data/mapper/payment/iban.dart'; import 'package:pshared/data/mapper/payment/russian_bank.dart'; import 'package:pshared/data/mapper/payment/type.dart'; import 'package:pshared/data/mapper/payment/wallet.dart'; +import 'package:pshared/models/describable.dart'; import 'package:pshared/models/organization/bound.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/type.dart'; import 'package:pshared/models/payment/methods/wallet.dart'; -import 'package:pshared/models/payment/payment_method.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/permissions/bound.dart'; import 'package:pshared/models/storable.dart'; -extension PaymentMethodModelMapper on PaymentMethodModel { +extension PaymentMethodMapper on PaymentMethod { PaymentMethodDTO toDTO() => PaymentMethodDTO( id: storable.id, createdAt: storable.createdAt, @@ -34,6 +35,9 @@ extension PaymentMethodModelMapper on PaymentMethodModel { type: paymentTypeToValue(type), data: _dataToJson(data), isArchived: isArchived, + name: describable.name, + description: describable.description, + isMain: isMain, ); Map _dataToJson(PaymentMethodData data) { @@ -53,7 +57,7 @@ extension PaymentMethodModelMapper on PaymentMethodModel { } extension PaymentMethodDTOMapper on PaymentMethodDTO { - PaymentMethodModel toDomain() => PaymentMethodModel( + PaymentMethod toDomain() => PaymentMethod( storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), permissionBound: newPermissionBound( organizationBound: newOrganizationBound(organizationRef: organizationRef), @@ -62,6 +66,8 @@ extension PaymentMethodDTOMapper on PaymentMethodDTO { recipientRef: recipientRef, data: _dataToDomain(paymentTypeFromValue(type), data), isArchived: isArchived, + describable: newDescribable(name: name, description: description), + isMain: isMain, ); PaymentMethodData _dataToDomain(PaymentType paymentType, Map payload) { diff --git a/frontend/pshared/lib/data/mapper/recipient/recipient.dart b/frontend/pshared/lib/data/mapper/recipient/recipient.dart index f77d477..020385c 100644 --- a/frontend/pshared/lib/data/mapper/recipient/recipient.dart +++ b/frontend/pshared/lib/data/mapper/recipient/recipient.dart @@ -2,13 +2,13 @@ import 'package:pshared/data/dto/recipient/recipient.dart'; import 'package:pshared/models/describable.dart'; import 'package:pshared/models/organization/bound.dart'; import 'package:pshared/models/permissions/bound.dart'; -import 'package:pshared/models/recipient/recipient_model.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/models/storable.dart'; -extension RecipientModelMapper on RecipientModel { +extension RecipientModelMapper on Recipient { RecipientDTO toDTO() => RecipientDTO( id: storable.id, createdAt: storable.createdAt, @@ -26,7 +26,7 @@ extension RecipientModelMapper on RecipientModel { } extension RecipientDTOMapper on RecipientDTO { - RecipientModel toDomain() => RecipientModel( + Recipient toDomain() => Recipient( storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), permissionBound: newPermissionBound( organizationBound: newOrganizationBound(organizationRef: organizationRef), diff --git a/frontend/pshared/lib/l10n/en.arb b/frontend/pshared/lib/l10n/en.arb index 53fa531..80764ee 100644 --- a/frontend/pshared/lib/l10n/en.arb +++ b/frontend/pshared/lib/l10n/en.arb @@ -19,5 +19,15 @@ "operationStatusError": "Error", "@operationStatusError": { "description": "Label for the “error” operation status" + }, + + "resourceLoadError": "Error while loading data. Try again", + "@resourceLoadError": { + "description": "Default message shown when data loading fails" + }, + + "resourceEmpty": "Empty data", + "@resourceEmpty": { + "description": "Default message shown when no data is available" } } diff --git a/frontend/pshared/lib/l10n/ru.arb b/frontend/pshared/lib/l10n/ru.arb index eb3ad71..0e28cdf 100644 --- a/frontend/pshared/lib/l10n/ru.arb +++ b/frontend/pshared/lib/l10n/ru.arb @@ -19,5 +19,15 @@ "operationStatusError": "Ошибка", "@operationStatusError": { "description": "Label for the “error” operation status" + }, + + "resourceLoadError": "Ошибка при загрузке данных. Попробуйте еще раз", + "@resourceLoadError": { + "description": "Default message shown when data loading fails" + }, + + "resourceEmpty": "Нет данных", + "@resourceEmpty": { + "description": "Default message shown when no data is available" } } diff --git a/frontend/pshared/lib/models/payment/methods/type.dart b/frontend/pshared/lib/models/payment/methods/type.dart index cb55fc8..b05a3de 100644 --- a/frontend/pshared/lib/models/payment/methods/type.dart +++ b/frontend/pshared/lib/models/payment/methods/type.dart @@ -1,21 +1,62 @@ +import 'package:pshared/models/describable.dart'; import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/permissions/bound.dart'; +import 'package:pshared/models/permissions/bound/storable.dart'; +import 'package:pshared/models/storable.dart'; -class PaymentMethod { - PaymentMethod({ - required this.id, - required this.label, - required this.details, - required this.type, - this.isEnabled = true, + +class PaymentMethod implements PermissionBoundStorable, Describable { + final Storable storable; + final PermissionBound permissionBound; + final Describable describable; + final String recipientRef; + final PaymentMethodData data; + final bool isArchived; + final bool isMain; + + const PaymentMethod({ + required this.storable, + required this.permissionBound, + required this.describable, + required this.recipientRef, + required this.data, + this.isArchived = false, this.isMain = false, }); - final String id; - final String label; - final String details; - final PaymentType type; + PaymentType get type => data.type; - bool isEnabled; - bool isMain; -} \ No newline at end of file + @override + String get id => storable.id; + @override + DateTime get createdAt => storable.createdAt; + @override + DateTime get updatedAt => storable.updatedAt; + + @override + String get organizationRef => permissionBound.organizationRef; + @override + String get permissionRef => permissionBound.permissionRef; + + @override + String get name => describable.name; + @override + String? get description => describable.description; + + PaymentMethod copyWith({ + PaymentMethodData? data, + bool? isArchived, + bool? isMain, + Describable? describable, + }) => PaymentMethod( + storable: storable, + permissionBound: permissionBound, + recipientRef: recipientRef, + data: data ?? this.data, + isArchived: isArchived ?? this.isArchived, + isMain: isMain ?? this.isMain, + describable: describable ?? this.describable, + ); +} diff --git a/frontend/pshared/lib/models/payment/payment_method.dart b/frontend/pshared/lib/models/payment/payment_method.dart deleted file mode 100644 index 0ca888a..0000000 --- a/frontend/pshared/lib/models/payment/payment_method.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:pshared/models/payment/methods/data.dart'; -import 'package:pshared/models/payment/type.dart'; -import 'package:pshared/models/permissions/bound.dart'; -import 'package:pshared/models/storable.dart'; - - -class PaymentMethodModel implements PermissionBound, Storable { - final Storable storable; - final PermissionBound permissionBound; - final String recipientRef; - final PaymentMethodData data; - final bool isArchived; - - const PaymentMethodModel({ - required this.storable, - required this.permissionBound, - required this.recipientRef, - required this.data, - this.isArchived = false, - }); - - PaymentType get type => data.type; - - @override - String get id => storable.id; - @override - DateTime get createdAt => storable.createdAt; - @override - DateTime get updatedAt => storable.updatedAt; - - @override - String get organizationRef => permissionBound.organizationRef; - @override - String get permissionRef => permissionBound.permissionRef; - - PaymentMethodModel copyWith({ - PaymentMethodData? data, - bool? isArchived, - }) => PaymentMethodModel( - storable: storable, - permissionBound: permissionBound, - recipientRef: recipientRef, - data: data ?? this.data, - isArchived: isArchived ?? this.isArchived, - ); -} diff --git a/frontend/pshared/lib/models/recipient/recipient.dart b/frontend/pshared/lib/models/recipient/recipient.dart index 43cdbfc..9ffeea2 100644 --- a/frontend/pshared/lib/models/recipient/recipient.dart +++ b/frontend/pshared/lib/models/recipient/recipient.dart @@ -1,86 +1,97 @@ -import 'package:pshared/models/payment/methods/card.dart'; -import 'package:pshared/models/payment/methods/iban.dart'; -import 'package:pshared/models/payment/methods/crypto_address.dart'; -import 'package:pshared/models/payment/methods/russian_bank.dart'; -import 'package:pshared/models/payment/methods/wallet.dart'; +import 'package:pshared/models/describable.dart'; +import 'package:pshared/models/organization/bound.dart'; +import 'package:pshared/models/permissions/bound/describable.dart'; +import 'package:pshared/models/permissions/bound.dart'; import 'package:pshared/models/recipient/status.dart'; import 'package:pshared/models/recipient/type.dart'; +import 'package:pshared/models/storable.dart'; -class Recipient { - final String? avatarUrl; // network URL / local asset - final String name; +class Recipient implements PermissionBoundStorableDescribable { + final Storable storable; + final PermissionBound permissionBound; + final Describable describable; final String email; + final String? avatarUrl; final RecipientStatus status; final RecipientType type; - final CardPaymentMethod? card; - final IbanPaymentMethod? iban; - final RussianBankAccountPaymentMethod? bank; - final WalletPaymentMethod? wallet; - final CryptoAddressPaymentMethod? cryptoAddress; + final bool isArchived; const Recipient({ - this.avatarUrl, - required this.name, + required this.storable, + required this.permissionBound, + required this.describable, required this.email, required this.status, required this.type, - this.card, - this.iban, - this.bank, - this.wallet, - this.cryptoAddress, + this.avatarUrl, + this.isArchived = false, }); - /// Convenience factory for quickly creating mock recipients. - factory Recipient.mock({ - required String name, - required String email, - required RecipientStatus status, - required RecipientType type, - CardPaymentMethod? card, - IbanPaymentMethod? iban, - RussianBankAccountPaymentMethod? bank, - WalletPaymentMethod? wallet, - CryptoAddressPaymentMethod? cryptoAddress, - }) => - Recipient( - avatarUrl: null, - name: name, - email: email, - status: status, - type: type, - card: card, - iban: iban, - bank: bank, - wallet: wallet, - cryptoAddress: cryptoAddress, - ); + @override + String get id => storable.id; + @override + DateTime get createdAt => storable.createdAt; + @override + DateTime get updatedAt => storable.updatedAt; + @override + String get organizationRef => permissionBound.organizationRef; + @override + String get permissionRef => permissionBound.permissionRef; + + @override + String get name => describable.name; + @override + String? get description => describable.description; + + Recipient copyWith({ + Describable? describable, + String? email, + String? Function()? avatarUrl, + RecipientStatus? status, + RecipientType? type, + bool? isArchived, + }) => Recipient( + storable: storable, + permissionBound: permissionBound, + describable: describableCopyWithOther(this.describable, describable), + email: email ?? this.email, + avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl, + status: status ?? this.status, + type: type ?? this.type, + isArchived: isArchived ?? this.isArchived, + ); + + // TODO: move search to backend bool matchesQuery(String q) { final searchable = [ name, email, - card?.pan, - card?.firstName, - card?.lastName, - iban?.iban, - iban?.accountHolder, - iban?.bic, - iban?.bankName, - bank?.accountNumber, - bank?.recipientName, - bank?.inn, - bank?.kpp, - bank?.bankName, - bank?.bik, - bank?.correspondentAccount, - wallet?.walletId, - cryptoAddress?.address, - cryptoAddress?.network, - cryptoAddress?.destinationTag, ]; - return searchable.any((field) => field?.toLowerCase().contains(q) ?? false); + return searchable.any((field) => field.toLowerCase().contains(q.toLowerCase())); } + } + +Recipient newRecipient({ + required String organizationRef, + required String email, + required String name, + required RecipientStatus status, + required RecipientType type, + String? description, + String? avatarUrl, + bool isArchived = false, +}) => + Recipient( + storable: newStorable(), + permissionBound: newPermissionBound(organizationBound: newOrganizationBound(organizationRef: organizationRef)), + describable: newDescribable(name: name, description: description), + email: email, + status: status, + type: type, + avatarUrl: avatarUrl, + isArchived: isArchived, + ); \ No newline at end of file diff --git a/frontend/pshared/lib/models/recipient/recipient_model.dart b/frontend/pshared/lib/models/recipient/recipient_model.dart deleted file mode 100644 index b9a6fd7..0000000 --- a/frontend/pshared/lib/models/recipient/recipient_model.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:pshared/models/describable.dart'; -import 'package:pshared/models/permissions/bound/describable.dart'; -import 'package:pshared/models/permissions/bound.dart'; -import 'package:pshared/models/recipient/status.dart'; -import 'package:pshared/models/recipient/type.dart'; -import 'package:pshared/models/storable.dart'; - - -class RecipientModel implements PermissionBoundStorableDescribable { - final Storable storable; - final PermissionBound permissionBound; - final Describable describable; - final String email; - final String? avatarUrl; - final RecipientStatus status; - final RecipientType type; - final bool isArchived; - - const RecipientModel({ - required this.storable, - required this.permissionBound, - required this.describable, - required this.email, - required this.status, - required this.type, - this.avatarUrl, - this.isArchived = false, - }); - - @override - String get id => storable.id; - @override - DateTime get createdAt => storable.createdAt; - @override - DateTime get updatedAt => storable.updatedAt; - - @override - String get organizationRef => permissionBound.organizationRef; - @override - String get permissionRef => permissionBound.permissionRef; - - @override - String get name => describable.name; - @override - String? get description => describable.description; - - RecipientModel copyWith({ - Describable? describable, - String? email, - String? Function()? avatarUrl, - RecipientStatus? status, - RecipientType? type, - bool? isArchived, - }) => RecipientModel( - storable: storable, - permissionBound: permissionBound, - describable: describableCopyWithOther(this.describable, describable), - email: email ?? this.email, - avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl, - status: status ?? this.status, - type: type ?? this.type, - isArchived: isArchived ?? this.isArchived, - ); -} diff --git a/frontend/pshared/lib/provider/recipient/pmethods.dart b/frontend/pshared/lib/provider/recipient/pmethods.dart new file mode 100644 index 0000000..c28932b --- /dev/null +++ b/frontend/pshared/lib/provider/recipient/pmethods.dart @@ -0,0 +1,59 @@ +import 'package:collection/collection.dart'; + +import 'package:pshared/data/mapper/payment/method.dart'; +import 'package:pshared/models/payment/methods/type.dart'; +import 'package:pshared/provider/organizations.dart'; +import 'package:pshared/provider/recipient/provider.dart'; +import 'package:pshared/provider/template.dart'; +import 'package:pshared/service/recipient/pmethods.dart'; + + +class PaymentMethodsProvider extends GenericProvider { + late OrganizationsProvider _organizations; + late RecipientsProvider _recipients; + + PaymentMethodsProvider() : super(service: PaymentMethodService.basicService); + + List get methods => List.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt))); + + void updateProviders(OrganizationsProvider organizations, RecipientsProvider recipients) { + _organizations = organizations; + _recipients = recipients; + if (_organizations.isOrganizationSet && (_recipients.currentObject != null)) { + load(_organizations.current.id, _recipients.currentObject!.id); + } + } + + // void reorderMethods(int oldIndex, int newIndex) { + // if (newIndex > oldIndex) newIndex--; + // final item = _methods.removeAt(oldIndex); + // _methods.insert(newIndex, item); + // notifyListeners(); + // } + + PaymentMethod? get main => methods.firstWhereOrNull((m) => m.isMain); + + Future updateMethod(PaymentMethod method) async => update(method.toDTO().toJson()); + + Future setArchivedMethod({ + required PaymentMethod method, + required bool newIsArchived, + }) async => setArchived( + organizationRef: _organizations.current.id, + objectRef: method.id, + newIsArchived: newIsArchived, + cascade: true, + ); + + + Future makeMain(PaymentMethod method) { + // TODO: create separate backend method to manage main payment method + final updates = >[]; + final currentMain = main; + if (currentMain != null) { + updates.add(updateMethod(currentMain.copyWith(isMain: false))); + } + updates.add(updateMethod(method.copyWith(isMain: true))); + return Future.wait(updates).then((_) => null); + } +} diff --git a/frontend/pweb/lib/providers/recipient.dart b/frontend/pshared/lib/provider/recipient/provider.dart similarity index 54% rename from frontend/pweb/lib/providers/recipient.dart rename to frontend/pshared/lib/provider/recipient/provider.dart index c5d72d2..6249291 100644 --- a/frontend/pweb/lib/providers/recipient.dart +++ b/frontend/pshared/lib/provider/recipient/provider.dart @@ -1,34 +1,27 @@ -import 'package:flutter/foundation.dart'; import 'package:pshared/models/recipient/filter.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/status.dart'; - -import 'package:pweb/services/recipient/recipient.dart'; +import 'package:pshared/provider/organizations.dart'; +import 'package:pshared/provider/template.dart'; +import 'package:pshared/service/recipient/service.dart'; -class RecipientProvider extends ChangeNotifier { - final RecipientService _service; +class RecipientsProvider extends GenericProvider { + late OrganizationsProvider _organizations; - RecipientProvider(this._service); - - List _recipients = []; - bool _isLoading = false; - String? _error; RecipientFilter _selectedFilter = RecipientFilter.all; String _query = ''; - Recipient? _selectedRecipient; - - List get recipients => _recipients; - bool get isLoading => _isLoading; - String? get error => _error; RecipientFilter get selectedFilter => _selectedFilter; String get query => _query; - Recipient? get selectedRecipient => _selectedRecipient; + + List get recipients => List.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt))); + + RecipientsProvider() : super(service: RecipientService.basicService); List get filteredRecipients { - List filtered = _recipients.where((r) { + List filtered = recipients.where((r) { switch (_selectedFilter) { case RecipientFilter.ready: return r.status == RecipientStatus.ready; @@ -48,21 +41,6 @@ class RecipientProvider extends ChangeNotifier { return filtered; } - Future loadRecipients() async { - _isLoading = true; - _error = null; - notifyListeners(); - - try { - _recipients = await _service.fetchRecipients(); - } catch (e) { - _error = e.toString(); - } finally { - _isLoading = false; - notifyListeners(); - } - } - void setFilter(RecipientFilter filter) { _selectedFilter = filter; notifyListeners(); @@ -73,8 +51,10 @@ class RecipientProvider extends ChangeNotifier { notifyListeners(); } - void selectRecipient(Recipient? recipient) { - _selectedRecipient = recipient; - notifyListeners(); + void updateProviders(OrganizationsProvider organizations) { + _organizations = organizations; + if (_organizations.isOrganizationSet) { + load(_organizations.current.id, _organizations.current.id); + } } } diff --git a/frontend/pshared/lib/provider/template.dart b/frontend/pshared/lib/provider/template.dart index c26e89c..c83d137 100644 --- a/frontend/pshared/lib/provider/template.dart +++ b/frontend/pshared/lib/provider/template.dart @@ -32,21 +32,23 @@ List mergeLists({ /// to manage state (loading, error, data) without re‑implementing service logic. class GenericProvider extends ChangeNotifier { final BasicService service; + bool _isLoaded = false; Resource> _resource = Resource(data: []); Resource> get resource => _resource; List get items => List.unmodifiable(_resource.data ?? []); bool get isLoading => _resource.isLoading; - bool get isEmpty => items.isEmpty; Object? get error => _resource.error; + bool get isReady => (error == null) && _isLoaded; + bool get isCurrentSet => _currentObjectRef != null; String? _currentObjectRef; // Stores the currently selected project ref T? get currentObject => _resource.data?.firstWhereOrNull( (object) => object.id == _currentObjectRef, ); - T? getItemById(String id) => items.firstWhereOrNull((item) => item.id == id); + T? getItemByRef(String id) => items.firstWhereOrNull((item) => item.id == id); GenericProvider({required this.service}); @@ -67,11 +69,13 @@ class GenericProvider extends ChangeNotifier notifyListeners(); } - Future loadFuture(Future> future) async { + Future> loadFuture(Future> future) async { _setResource(_resource.copyWith(isLoading: true)); try { final list = await future; + _isLoaded = true; _setResource(Resource(data: list, isLoading: false)); + return list; } catch (e) { _setResource( _resource.copyWith(isLoading: false, error: toException(e)), @@ -80,17 +84,30 @@ class GenericProvider extends ChangeNotifier } } - Future load(String organizationRef, String? parentRef) async { + Future load( + String organizationRef, + String? parentRef, { + int? limit, + int? offset, + bool? Function()? fetchArchived, + }) async { if (parentRef != null) { - return loadFuture(service.list(organizationRef, parentRef)); + await loadFuture( + service.list( + organizationRef, + parentRef, + limit: limit, + offset: offset, + fetchArchived: fetchArchived == null ? null : fetchArchived(), + ), + ); } } Future loadItem(String itemRef) async { - return loadFuture((() async => [await service.get(itemRef)])()); + await loadFuture((() async => [await service.get(itemRef)])()); } - List merge(List rhs) => mergeLists( lhs: items, rhs: rhs, @@ -134,11 +151,47 @@ class GenericProvider extends ChangeNotifier } } - Future delete(String objectRef) async { + Future delete(String objectRef, {Map? request}) async { _setResource(_resource.copyWith(isLoading: true)); try { - await service.delete(objectRef); + await service.delete(objectRef, request: request); + if (_currentObjectRef == objectRef) { + _currentObjectRef = null; + } + + _setResource(Resource( + data: _resource.data?.where((p) => p.id != objectRef).toList(), + isLoading: false, + )); + } catch (e) { + _setResource(Resource(data: _resource.data, isLoading: false, error: toException(e))); + rethrow; + } + } + + Future toggleArchived(T item, bool currentState, {bool? cascade}) => setArchived( + organizationRef: item.organizationRef, + objectRef: item.id, + newIsArchived: !currentState, + cascade: cascade ?? true, + ); + + Future setArchived({ + required String organizationRef, + required String objectRef, + required bool newIsArchived, + bool? cascade, + }) async { + _setResource(_resource.copyWith(isLoading: true)); + + try { + await service.archive( + organizationRef: organizationRef, + objectRef: objectRef, + newIsArchived: newIsArchived, + cascade: cascade, + ); if (_currentObjectRef == objectRef) { _currentObjectRef = null; } @@ -154,11 +207,17 @@ class GenericProvider extends ChangeNotifier } bool setCurrentObject(String? objectRef) { + if (_currentObjectRef == objectRef) { + // No change, skip notification + return true; + } + if (objectRef == null) { _currentObjectRef = null; notifyListeners(); return true; } + if (_resource.data?.any((p) => p.id == objectRef) ?? false) { _currentObjectRef = objectRef; notifyListeners(); @@ -167,4 +226,5 @@ class GenericProvider extends ChangeNotifier return false; // Object not found } + } diff --git a/frontend/pshared/lib/service/recipient/pmethods.dart b/frontend/pshared/lib/service/recipient/pmethods.dart new file mode 100644 index 0000000..3d629cd --- /dev/null +++ b/frontend/pshared/lib/service/recipient/pmethods.dart @@ -0,0 +1,37 @@ +import 'package:pshared/api/responses/payment_method.dart'; +import 'package:pshared/data/mapper/payment/method.dart'; +import 'package:pshared/models/payment/methods/type.dart'; +import 'package:pshared/service/services.dart'; +import 'package:pshared/service/template.dart'; + + +class PaymentMethodService { + static const String _objectType = Services.paymentMethods; + + static final BasicService _basicService = BasicService( + objectType: _objectType, + fromJson: (json) => PaymentMethodResponse.fromJson(json).paymentMethods.map((dto) => dto.toDomain()).toList(), + ); + + static BasicService get basicService => _basicService; + + static Future> list(String organizationRef, String recipientRef) async { + return _basicService.list(organizationRef, recipientRef); + } + + static Future get(String recipientRef) async { + return _basicService.get(recipientRef); + } + + static Future> create(String organizationRef, PaymentMethod paymentMethod) async { + return _basicService.create(organizationRef, paymentMethod.toDTO().toJson()); + } + + static Future> update(PaymentMethod paymentMethod) async { + return _basicService.update(paymentMethod.toDTO().toJson()); + } + + static Future> delete(PaymentMethod paymentMethod) async { + return _basicService.delete(paymentMethod.storable.id); + } +} diff --git a/frontend/pshared/lib/service/recipient/service.dart b/frontend/pshared/lib/service/recipient/service.dart new file mode 100644 index 0000000..8c13fb8 --- /dev/null +++ b/frontend/pshared/lib/service/recipient/service.dart @@ -0,0 +1,37 @@ +import 'package:pshared/api/responses/recipient.dart'; +import 'package:pshared/data/mapper/recipient/recipient.dart'; +import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/service/services.dart'; +import 'package:pshared/service/template.dart'; + + +class RecipientService { + static const String _objectType = Services.recipients; + + static final BasicService _basicService = BasicService( + objectType: _objectType, + fromJson: (json) => RecipientResponse.fromJson(json).recipients.map((dto) => dto.toDomain()).toList(), + ); + + static BasicService get basicService => _basicService; + + static Future> list(String organizationRef, String _) async { + return _basicService.list(organizationRef, organizationRef); + } + + static Future get(String recipientRef) async { + return _basicService.get(recipientRef); + } + + static Future> create(String organizationRef, Recipient recipient) async { + return _basicService.create(organizationRef, recipient.toDTO().toJson()); + } + + static Future> update(Recipient recipient) async { + return _basicService.update(recipient.toDTO().toJson()); + } + + static Future> delete(Recipient recipient) async { + return _basicService.delete(recipient.storable.id); + } +} diff --git a/frontend/pshared/lib/service/template.dart b/frontend/pshared/lib/service/template.dart index d61a2aa..7a65cc5 100644 --- a/frontend/pshared/lib/service/template.dart +++ b/frontend/pshared/lib/service/template.dart @@ -1,6 +1,7 @@ import 'package:logging/logging.dart'; import 'package:pshared/service/authorization/service.dart'; +import 'package:pshared/utils/http/params.dart'; class BasicService { @@ -15,15 +16,26 @@ class BasicService { required this.fromJson, }) : _objectType = objectType, _logger = Logger('service.$objectType'); - Future> list(String organizationRef, String parentRef) async { - _logger.fine('Loading all objects'); + String _refLog(String ref) => ref.isEmpty ? '' : ref; + + Future> list(String organizationRef, String parentRef, {int? limit, int? offset, bool? fetchArchived}) async { + _logger.fine('Loading all for organization ${_refLog(organizationRef)} and parent ${_refLog(organizationRef)} with: limit=${_formatParameter(limit)}, offset=${_formatParameter(offset)}, fetchArchived=${_formatParameter(fetchArchived)}...'); + return _getObjects( - AuthorizationService.getGETResponse(_objectType, '/list/$organizationRef/$parentRef'), + AuthorizationService.getGETResponse( + _objectType, + paramsToUriString( + path: '/list/$organizationRef/$parentRef', + limit: limit, + offset: offset, + fetchArchived: fetchArchived, + ), + ), ); } Future get(String objectRef) async { - _logger.fine('Loading object $objectRef'); + _logger.fine('Loading $_objectType $objectRef'); final objects = await _getObjects( AuthorizationService.getGETResponse(_objectType, '/$objectRef'), ); @@ -31,24 +43,36 @@ class BasicService { } Future> create(String organizationRef, Map request) async { - _logger.fine('Creating new object...'); + _logger.fine('Creating new...'); return _getObjects( AuthorizationService.getPOSTResponse(_objectType, '/$organizationRef', request), ); } Future> update(Map request) async { - _logger.fine('Patching object...'); + _logger.fine('Patching...'); return _getObjects( AuthorizationService.getPUTResponse(_objectType, '/', request, ), ); } - Future> delete(String objecRef) async { - _logger.fine('Deleting object $objecRef'); + Future> delete(String objecRef, {Map? request}) async { + _logger.fine('Deleting $_objectType $objecRef'); return _getObjects( - AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', {}), + AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', request ?? {}), + ); + } + + Future> archive({ + required String organizationRef, + required String objectRef, + required bool newIsArchived, + bool? cascade, + }) async { + _logger.fine('Setting new archive status $newIsArchived to $objectRef'); + return _getObjects( + AuthorizationService.getGETResponse(_objectType, '/archive/$organizationRef/$objectRef?archived=$newIsArchived&cascade=${cascade ?? false}'), ); } @@ -59,8 +83,12 @@ class BasicService { _logger.fine('Fetched ${objects.length} object(s)'); return objects; } catch (e, stackTrace) { - _logger.severe('Failed to fetch objects', e, stackTrace); + _logger.severe('Failed to fetch', e, stackTrace); rethrow; } } + + String _formatParameter(dynamic value) { + return value?.toString() ?? ''; + } } diff --git a/frontend/pshared/lib/utils/http/params.dart b/frontend/pshared/lib/utils/http/params.dart new file mode 100644 index 0000000..195eb77 --- /dev/null +++ b/frontend/pshared/lib/utils/http/params.dart @@ -0,0 +1,38 @@ +// Query parameter constants +const String _limitParam = 'limit'; +const String _offsetParam = 'offset'; +const String _archivedParam = 'archived'; + +void _addIfNotNull(Map params, String key, dynamic value) { + if (value != null) { + params[key] = value.toString(); + } +} + +Uri paramsToUri({ + required String path, + int? limit, + int? offset, + bool? fetchArchived, + Map? params, +}) { + Map queryParams = params ?? {}; + _addIfNotNull(queryParams, _limitParam, limit); + _addIfNotNull(queryParams, _offsetParam, offset); + _addIfNotNull(queryParams, _archivedParam, fetchArchived); + + // Build URL with query parameters using Uri class + return Uri( + path: path, + queryParameters: queryParams.isEmpty ? null : queryParams, + ); +} + + +String paramsToUriString({ + required String path, + Map queryParams = const {}, + int? limit, + int? offset, + bool? fetchArchived, +}) => paramsToUri(path: path, limit: limit, offset: offset, fetchArchived: fetchArchived).toString(); diff --git a/frontend/pshared/lib/widgets/template.dart b/frontend/pshared/lib/widgets/template.dart index 9041c88..47e2f65 100644 --- a/frontend/pshared/lib/widgets/template.dart +++ b/frontend/pshared/lib/widgets/template.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/generated/i18n/ps_localizations.dart'; import 'package:pshared/provider/template.dart'; @@ -20,9 +21,10 @@ class ResourceContainer extends StatelessWidget { @override Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { - if (provider.isLoading) return loading ?? Center(child: CircularProgressIndicator()); - if (provider.error != null) return error ?? Text('Error while loading data. Try again'); - if (provider.isEmpty) return empty ?? Text('Empty data'); + final l10n = PSLocalizations.of(context)!; + if (provider.isLoading) return loading ?? const Center(child: CircularProgressIndicator()); + if (provider.error != null) return error ?? Text(l10n.resourceLoadError); + if (provider.items.isEmpty) return empty ?? Text(l10n.resourceEmpty); return builder(context, provider); }); } diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 6200e91..6f271e3 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -311,6 +311,7 @@ "paymentTypeBankAccount": "Russian Bank Account", "paymentTypeIban": "IBAN", "paymentTypeWallet": "Wallet", + "paymentTypeCryptoAddress": "Crypto address", "cardNumber": "Card Number", "enterCardNumber": "Enter the card number", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 3d4ea3d..8c41917 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -311,6 +311,7 @@ "paymentTypeBankAccount": "Российский банковский счет", "paymentTypeIban": "IBAN", "paymentTypeWallet": "Кошелек", + "paymentTypeCryptoAddress": "Крипто-адрес", "cardNumber": "Номер карты", "enterCardNumber": "Введите номер карты", diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 9ecdaef..c75b50d 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -12,6 +12,8 @@ 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/recipient/provider.dart'; +import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pweb/app/app.dart'; import 'package:pweb/app/timeago.dart'; @@ -19,17 +21,13 @@ import 'package:pweb/providers/carousel.dart'; import 'package:pweb/providers/mock_payment.dart'; import 'package:pweb/providers/operatioins.dart'; import 'package:pweb/providers/page_selector.dart'; -import 'package:pweb/providers/payment_methods.dart'; -import 'package:pweb/providers/recipient.dart'; 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/payment_methods.dart'; -import 'package:pweb/services/payments/upload_history.dart'; -import 'package:pweb/services/recipient/recipient.dart'; +import 'package:pweb/services/payments/history.dart'; import 'package:pweb/services/wallet_transactions.dart'; import 'package:pweb/services/wallets.dart'; @@ -77,8 +75,13 @@ void main() async { ChangeNotifierProvider( create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(), ), - ChangeNotifierProvider( - create: (_) => PaymentMethodsProvider(service: MockPaymentMethodsService())..loadMethods(), + ChangeNotifierProxyProvider( + create: (_) => RecipientsProvider(), + update: (context, organizations, provider) => provider!..updateProviders(organizations), + ), + ChangeNotifierProxyProvider2( + create: (_) => PaymentMethodsProvider(), + update: (context, organizations, recipients, provider) => provider!..updateProviders(organizations, recipients), ), ChangeNotifierProxyProvider( create: (_) => WalletsProvider(ApiWalletsService()), @@ -90,18 +93,11 @@ void main() async { ChangeNotifierProvider( create: (_) => MockPaymentProvider(), ), - ChangeNotifierProvider( - create: (_) => RecipientProvider(RecipientService())..loadRecipients(), - ), - ChangeNotifierProxyProvider3( + ChangeNotifierProxyProvider3( create: (context) => PageSelectorProvider(), update: (context, recipientProv, walletsProv, methodsProv, previous) => - previous ?? PageSelectorProvider( - recipientProvider: recipientProv, - walletsProvider: walletsProv, - methodsProvider: methodsProv, - )..update(recipientProv, walletsProv, methodsProv), + previous ?? PageSelectorProvider()..update(recipientProv, walletsProv, methodsProv), ), ChangeNotifierProvider( diff --git a/frontend/pweb/lib/pages/address_book/form/page.dart b/frontend/pweb/lib/pages/address_book/form/page.dart index 635a3b5..4133a69 100644 --- a/frontend/pweb/lib/pages/address_book/form/page.dart +++ b/frontend/pweb/lib/pages/address_book/form/page.dart @@ -1,5 +1,7 @@ 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'; @@ -9,6 +11,7 @@ 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:pweb/pages/address_book/form/view.dart'; import 'package:pweb/services/amplitude.dart'; @@ -38,8 +41,8 @@ class _AdressBookRecipientFormState extends State { void initState() { super.initState(); final r = widget.recipient; - _nameCtrl = TextEditingController(text: r?.name ?? ""); - _emailCtrl = TextEditingController(text: r?.email ?? ""); + _nameCtrl = TextEditingController(text: r?.name ?? ''); + _emailCtrl = TextEditingController(text: r?.email ?? ''); _type = r?.type ?? RecipientType.internal; _status = r?.status ?? RecipientStatus.ready; @@ -50,7 +53,7 @@ class _AdressBookRecipientFormState extends State { if (r?.cryptoAddress != null) _methods[PaymentType.cryptoAddress] = r!.cryptoAddress; } - //TODO Change when registration is ready + //TODO: Change when registration is ready void _save() { if (!_formKey.currentState!.validate() || _methods.isEmpty) { AmplitudeService.recipientAddCompleted( @@ -66,47 +69,41 @@ class _AdressBookRecipientFormState extends State { return; } - final recipient = Recipient( + final recipient = newRecipient( name: _nameCtrl.text, email: _emailCtrl.text, type: _type, status: _status, avatarUrl: null, - card: _methods[PaymentType.card] as CardPaymentMethod?, - iban: _methods[PaymentType.iban] as IbanPaymentMethod?, - wallet: _methods[PaymentType.wallet] as WalletPaymentMethod?, - bank: _methods[PaymentType.bankAccount] as RussianBankAccountPaymentMethod?, - cryptoAddress: _methods[PaymentType.cryptoAddress] as CryptoAddressPaymentMethod?, + organizationRef: context.read().current.id ); widget.onSaved?.call(recipient); } @override - Widget build(BuildContext context) { - return FormView( - formKey: _formKey, - nameCtrl: _nameCtrl, - emailCtrl: _emailCtrl, - type: _type, - status: _status, - methods: _methods, - onTypeChanged: (t) => setState(() => _type = t), - onStatusChanged: (s) => setState(() => _status = s), - onMethodsChanged: (type, data) { - setState(() { - if (data != null) { - _methods[type] = data; - } else { - _methods.remove(type); - } - }); - }, - onSave: _save, - isEditing: widget.recipient != null, - onBack: () { - widget.onSaved?.call(null); - }, - ); - } + Widget build(BuildContext context) => FormView( + formKey: _formKey, + nameCtrl: _nameCtrl, + emailCtrl: _emailCtrl, + type: _type, + status: _status, + methods: _methods, + onTypeChanged: (t) => setState(() => _type = t), + onStatusChanged: (s) => setState(() => _status = s), + onMethodsChanged: (type, data) { + setState(() { + if (data != null) { + _methods[type] = data; + } else { + _methods.remove(type); + } + }); + }, + onSave: _save, + isEditing: widget.recipient != null, + onBack: () { + widget.onSaved?.call(null); + }, + ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart index bdcf814..1a5bd08 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart @@ -11,13 +11,11 @@ class PaymentInfoRow extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return Row( - children: [ - Text(label, style: Theme.of(context).textTheme.bodySmall), - const SizedBox(width: 8), - Text(value, style: Theme.of(context).textTheme.bodySmall), - ], - ); - } + Widget build(BuildContext context) => Row( + children: [ + Text(label, style: Theme.of(context).textTheme.bodySmall), + const SizedBox(width: 8), + Text(value, style: Theme.of(context).textTheme.bodySmall), + ], + ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/item.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/item.dart index c5dc447..fcbaac5 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/item.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/item.dart @@ -1,36 +1,60 @@ 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: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'; import 'package:pweb/utils/payment/label.dart'; -class RecipientItem extends StatelessWidget { - final Recipient recipient; - final VoidCallback onTap; - +class RecipientItem extends StatefulWidget { static const double _horizontalPadding = 16.0; static const double _verticalPadding = 8.0; static const double _avatarRadius = 20; static const double _spacingWidth = 12; + final Recipient recipient; + final VoidCallback onTap; + const RecipientItem({ super.key, required this.recipient, required this.onTap, }); + @override + State createState() => _RecipientItemState(); +} + +class _RecipientItemState extends State { + late PaymentMethodsProvider _methodsProvider; + + @override + void initState() { + super.initState(); + _methodsProvider = PaymentMethodsProvider(); + _methodsProvider.updateProviders( + context.read(), + context.read(), + ); + } + @override Widget build(BuildContext context) { + if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator()); + + final recipient = widget.recipient; return InkWell( - onTap: onTap, + onTap: widget.onTap, child: Padding( padding: const EdgeInsets.symmetric( - horizontal: _horizontalPadding, - vertical: _verticalPadding, + horizontal: RecipientItem._horizontalPadding, + vertical: RecipientItem._verticalPadding, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -42,43 +66,20 @@ class RecipientItem extends StatelessWidget { isVisible: false, name: recipient.name, avatarUrl: recipient.avatarUrl, - avatarRadius: _avatarRadius, + avatarRadius: RecipientItem._avatarRadius, nameStyle: Theme.of(context).textTheme.bodyMedium, ), title: Text(recipient.name), subtitle: Text(recipient.email), ), ), - const SizedBox(width: _spacingWidth), + const SizedBox(width: RecipientItem._spacingWidth), Column( crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (recipient.bank?.accountNumber.isNotEmpty == true) - PaymentInfoRow( - label: getPaymentTypeLabel(context, PaymentType.bankAccount), - value: recipient.bank!.accountNumber, - ), - if (recipient.card?.pan.isNotEmpty == true) - PaymentInfoRow( - label: getPaymentTypeLabel(context, PaymentType.card), - value: recipient.card!.pan, - ), - if (recipient.iban?.iban.isNotEmpty == true) - PaymentInfoRow( - label: getPaymentTypeLabel(context, PaymentType.iban), - value: recipient.iban!.iban, - ), - if (recipient.wallet?.walletId.isNotEmpty == true) - PaymentInfoRow( - label: getPaymentTypeLabel(context, PaymentType.wallet), - value: recipient.wallet!.walletId, - ), - if (recipient.cryptoAddress?.address.isNotEmpty == true) - PaymentInfoRow( - label: getPaymentTypeLabel(context, PaymentType.cryptoAddress), - value: recipient.cryptoAddress!.address, - ), - ], + children: _methodsProvider.methods.map((m) => PaymentInfoRow( + label: getPaymentTypeLabel(context, m.type), + value: _displayString(m), + )).toList(), ), ], ), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/widget.dart similarity index 100% rename from frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart rename to frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/widget.dart diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart index bceda13..92e6911 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/recipient/provider.dart'; import 'package:pweb/pages/address_book/page/search.dart'; -import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart'; +import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/widget.dart'; import 'package:pweb/pages/dashboard/payouts/single/adress_book/short_list.dart'; -import 'package:pweb/providers/recipient.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -39,7 +39,7 @@ class _AdressBookPayoutState extends State { @override void initState() { super.initState(); - final provider = context.read(); + final provider = context.read(); _searchController = TextEditingController(text: provider.query); _searchController.addListener(() { @@ -57,7 +57,7 @@ class _AdressBookPayoutState extends State { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; - final provider = context.watch(); + final provider = context.watch(); if (provider.isLoading) { return const Center(child: CircularProgressIndicator()); @@ -88,14 +88,14 @@ class _AdressBookPayoutState extends State { const SizedBox(height: _spacingBetween), Expanded( child: _isExpanded - ? LongListAdressBookPayout( - filteredRecipients: provider.filteredRecipients, - onSelected: widget.onSelected, - ) - : ShortListAdressBookPayout( - recipients: provider.recipients, - onSelected: widget.onSelected, - ), + ? LongListAdressBookPayout( + filteredRecipients: provider.filteredRecipients, + onSelected: widget.onSelected, + ) + : ShortListAdressBookPayout( + recipients: provider.recipients, + onSelected: widget.onSelected, + ), ), ], ), diff --git a/frontend/pweb/lib/pages/payment_methods/method_selector.dart b/frontend/pweb/lib/pages/payment_methods/method_selector.dart index a87c91c..8d2bae8 100644 --- a/frontend/pweb/lib/pages/payment_methods/method_selector.dart +++ b/frontend/pweb/lib/pages/payment_methods/method_selector.dart @@ -1,27 +1,25 @@ import 'package:flutter/material.dart'; -import 'package:pshared/models/payment/methods/type.dart'; +import 'package:provider/provider.dart'; + +import 'package:pshared/models/payment/methods/type.dart'; +import 'package:pshared/provider/recipient/pmethods.dart'; -import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/utils/payment/dropdown.dart'; class PaymentMethodSelector extends StatelessWidget { - final PaymentMethodsProvider methodsProvider; final ValueChanged onMethodChanged; const PaymentMethodSelector({ super.key, - required this.methodsProvider, required this.onMethodChanged, }); @override - Widget build(BuildContext context) { - return PaymentMethodDropdown( - methods: methodsProvider.methods, - initialValue: methodsProvider.selectedMethod, - onChanged: onMethodChanged, - ); - } + Widget build(BuildContext context) => Consumer(builder:(context, provider, _) => PaymentMethodDropdown( + methods: provider.methods, + initialValue: provider.currentObject, + onChanged: onMethodChanged, + )); } diff --git a/frontend/pweb/lib/pages/payment_methods/page.dart b/frontend/pweb/lib/pages/payment_methods/page.dart index bd25550..f83bd79 100644 --- a/frontend/pweb/lib/pages/payment_methods/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/page.dart @@ -3,12 +3,12 @@ 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'; import 'package:pweb/pages/payment_methods/widgets/payment_page_body.dart'; import 'package:pweb/providers/page_selector.dart'; -import 'package:pweb/providers/payment_methods.dart'; -import 'package:pweb/providers/recipient.dart'; class PaymentPage extends StatefulWidget { @@ -49,30 +49,22 @@ class _PaymentPageState extends State { void _initializePaymentPage() { final pageSelector = context.read(); final methodsProvider = context.read(); - final recipientProvider = context.read(); + final recipientProvider = context.read(); pageSelector.handleWalletAutoSelection(); - if (methodsProvider.methods.isEmpty && !methodsProvider.isLoading) { - methodsProvider.loadMethods(); - } - - if (recipientProvider.recipients.isEmpty && !recipientProvider.isLoading) { - recipientProvider.loadRecipients(); - } - _flowProvider.syncWithSelector(pageSelector); } void _handleSearchChanged(String query) { - context.read().setQuery(query); + context.read().setQuery(query); } void _handleRecipientSelected(Recipient recipient) { final pageSelector = context.read(); - final recipientProvider = context.read(); + final recipientProvider = context.read(); - recipientProvider.selectRecipient(recipient); + recipientProvider.setCurrentObject(recipient.id); pageSelector.selectRecipient(recipient); _flowProvider.reset(pageSelector); _clearSearchField(); @@ -80,9 +72,9 @@ class _PaymentPageState extends State { void _handleRecipientCleared() { final pageSelector = context.read(); - final recipientProvider = context.read(); + final recipientProvider = context.read(); - recipientProvider.selectRecipient(null); + recipientProvider.setCurrentObject(null); pageSelector.selectRecipient(null); _flowProvider.reset(pageSelector); _clearSearchField(); @@ -91,7 +83,7 @@ class _PaymentPageState extends State { void _clearSearchField() { _searchController.clear(); _searchFocusNode.unfocus(); - context.read().setQuery(''); + context.read().setQuery(''); } void _handleSendPayment() { diff --git a/frontend/pweb/lib/pages/payment_methods/title.dart b/frontend/pweb/lib/pages/payment_methods/title.dart index c14c93b..dc91e17 100644 --- a/frontend/pweb/lib/pages/payment_methods/title.dart +++ b/frontend/pweb/lib/pages/payment_methods/title.dart @@ -31,7 +31,7 @@ class PaymentMethodTile extends StatelessWidget { final theme = Theme.of(context); return Opacity( - opacity: method.isEnabled ? 1 : 0.5, + opacity: method.isArchived ? 1 : 0.5, child: Card( margin: const EdgeInsets.symmetric(vertical: 4), elevation: 0, @@ -41,11 +41,12 @@ class PaymentMethodTile extends StatelessWidget { onTap: makeMain, title: Row( children: [ - Expanded(child: Text(method.label)), - Text( - method.details, - style: theme.textTheme.bodySmall, - ), + Expanded(child: Text(method.name)), + if (method.description != null) + Text( + method.description!, + style: theme.textTheme.bodySmall, + ), ], ), trailing: Row( @@ -73,12 +74,10 @@ class PaymentMethodTile extends StatelessWidget { ); } - Widget _buildEnabledSwitch() { - return Switch.adaptive( - value: method.isEnabled, - onChanged: toggleEnabled, - ); - } + Widget _buildEnabledSwitch() => Switch.adaptive( + value: method.isArchived, + onChanged: toggleEnabled, + ); Widget _buildPopupMenu(AppLocalizations l10n) { return PopupMenuButton( diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart index 6479aac..e33dcfe 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart @@ -1,7 +1,10 @@ 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/pages/payment_methods/header.dart'; import 'package:pweb/pages/payment_methods/method_selector.dart'; @@ -12,9 +15,8 @@ import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart'; import 'package:pweb/pages/payment_methods/widgets/section_title.dart'; import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/providers/payment_flow_provider.dart'; -import 'package:pweb/providers/payment_methods.dart'; -import 'package:pweb/providers/recipient.dart'; import 'package:pweb/utils/dimensions.dart'; + import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -43,7 +45,7 @@ class PaymentPageBody extends StatelessWidget { final dimensions = AppDimensions(); final pageSelector = context.watch(); final methodsProvider = context.watch(); - final recipientProvider = context.watch(); + final recipientProvider = context.watch(); final flowProvider = context.watch(); final recipient = pageSelector.selectedRecipient; final loc = AppLocalizations.of(context)!; @@ -79,8 +81,7 @@ class PaymentPageBody extends StatelessWidget { SectionTitle(loc.sourceOfFunds), SizedBox(height: dimensions.paddingSmall), PaymentMethodSelector( - methodsProvider: methodsProvider, - onMethodChanged: methodsProvider.selectMethod, + onMethodChanged: (m) => methodsProvider.setCurrentObject(m.id), ), SizedBox(height: dimensions.paddingXLarge), diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart index 1d284fa..8d6e718 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/recipient/provider.dart'; import 'package:pweb/pages/address_book/page/search.dart'; import 'package:pweb/pages/payment_methods/widgets/card.dart'; import 'package:pweb/pages/payment_methods/widgets/search.dart'; import 'package:pweb/pages/payment_methods/widgets/section_title.dart'; -import 'package:pweb/providers/recipient.dart'; import 'package:pweb/utils/dimensions.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -15,7 +15,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class RecipientSection extends StatelessWidget { final Recipient? recipient; final AppDimensions dimensions; - final RecipientProvider recipientProvider; + final RecipientsProvider recipientProvider; final TextEditingController searchController; final FocusNode searchFocusNode; final ValueChanged onSearchChanged; diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/search.dart b/frontend/pweb/lib/pages/payment_methods/widgets/search.dart index c6acf25..c23dda4 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/search.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/search.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/recipient/provider.dart'; -import 'package:pweb/providers/recipient.dart'; import 'package:pweb/utils/dimensions.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -10,7 +10,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class RecipientSearchResults extends StatelessWidget { final AppDimensions dimensions; - final RecipientProvider recipientProvider; + final RecipientsProvider recipientProvider; final ValueChanged onRecipientSelected; const RecipientSearchResults({ diff --git a/frontend/pweb/lib/pages/payout_page/methods/controller.dart b/frontend/pweb/lib/pages/payout_page/methods/controller.dart index f964b0a..90c1809 100644 --- a/frontend/pweb/lib/pages/payout_page/methods/controller.dart +++ b/frontend/pweb/lib/pages/payout_page/methods/controller.dart @@ -4,8 +4,8 @@ import 'package:provider/provider.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/methods/type.dart'; +import 'package:pshared/provider/recipient/pmethods.dart'; -import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/pages/payment_methods/add/widget.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -16,18 +16,10 @@ class PaymentConfigController { PaymentConfigController(this.context); - void loadMethods() { - context.read().loadMethods(); - } - - Future addMethod() async { - final methodsProvider = context.read(); - await showDialog( - context: context, - builder: (_) => const AddPaymentMethodDialog(), - ); - methodsProvider.loadMethods(); - } + Future addMethod() async => showDialog( + context: context, + builder: (_) => const AddPaymentMethodDialog(), + ); Future editMethod(PaymentMethod method) async { // TODO: implement edit functionality @@ -55,12 +47,12 @@ class PaymentConfigController { ); if (confirmed == true) { - methodsProvider.deleteMethod(method); + methodsProvider.delete(method.id); } } void toggleEnabled(PaymentMethod method, bool value) { - context.read().toggleEnabled(method, value); + context.read().setArchivedMethod(method: method, newIsArchived: value); } void makeMain(PaymentMethod method) { @@ -68,6 +60,7 @@ class PaymentConfigController { } void reorder(int oldIndex, int newIndex) { - context.read().reorderMethods(oldIndex, newIndex); + // TODO: rimplement on top of Indexable + // context.read().reorderMethods(oldIndex, newIndex); } } diff --git a/frontend/pweb/lib/pages/payout_page/methods/list.dart b/frontend/pweb/lib/pages/payout_page/methods/list.dart index 911e255..8196d57 100644 --- a/frontend/pweb/lib/pages/payout_page/methods/list.dart +++ b/frontend/pweb/lib/pages/payout_page/methods/list.dart @@ -2,9 +2,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/provider/recipient/pmethods.dart'; + import 'package:pweb/pages/payment_methods/title.dart'; import 'package:pweb/pages/payout_page/methods/controller.dart'; -import 'package:pweb/providers/payment_methods.dart'; class PaymentConfigList extends StatelessWidget { diff --git a/frontend/pweb/lib/pages/payout_page/methods/widget.dart b/frontend/pweb/lib/pages/payout_page/methods/widget.dart index f54b81f..e0250c8 100644 --- a/frontend/pweb/lib/pages/payout_page/methods/widget.dart +++ b/frontend/pweb/lib/pages/payout_page/methods/widget.dart @@ -20,7 +20,6 @@ class _MethodsWidgetState extends State { void initState() { super.initState(); controller = PaymentConfigController(context); - controller.loadMethods(); } @override diff --git a/frontend/pweb/lib/pages/payout_page/page.dart b/frontend/pweb/lib/pages/payout_page/page.dart index 5ef4fbb..8095368 100644 --- a/frontend/pweb/lib/pages/payout_page/page.dart +++ b/frontend/pweb/lib/pages/payout_page/page.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pweb/models/wallet.dart'; +import 'package:pshared/provider/recipient/pmethods.dart'; +import 'package:pweb/models/wallet.dart'; import 'package:pweb/pages/payout_page/methods/widget.dart'; import 'package:pweb/pages/payout_page/wallet/wigets.dart'; -import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; diff --git a/frontend/pweb/lib/providers/page_selector.dart b/frontend/pweb/lib/providers/page_selector.dart index 7f1a6a5..3d1d4a9 100644 --- a/frontend/pweb/lib/providers/page_selector.dart +++ b/frontend/pweb/lib/providers/page_selector.dart @@ -1,41 +1,40 @@ -import 'package:collection/collection.dart'; - 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'; import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/recipient/pmethods.dart'; +import 'package:pshared/provider/recipient/provider.dart'; import 'package:pweb/models/wallet.dart'; -import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/providers/wallets.dart'; -import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/services/amplitude.dart'; -import 'package:pweb/providers/recipient.dart'; +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; PayoutDestination? _previousDestination; - RecipientProvider? recipientProvider; - WalletsProvider? walletsProvider; - PaymentMethodsProvider? methodsProvider; + late RecipientsProvider recipientProvider; + late WalletsProvider walletsProvider; + late PaymentMethodsProvider methodsProvider; PayoutDestination get selected => _selected; PaymentType? get type => _type; bool get cameFromRecipientList => _cameFromRecipientList; - PageSelectorProvider({ - this.recipientProvider, - this.walletsProvider, - this.methodsProvider, - }); + PageSelectorProvider(); void update( - RecipientProvider recipientProv, + RecipientsProvider recipientProv, WalletsProvider walletsProv, PaymentMethodsProvider methodsProv, ) { @@ -50,44 +49,30 @@ class PageSelectorProvider extends ChangeNotifier { } void selectRecipient(Recipient? recipient, {bool fromList = false}) { - if (recipientProvider != null) { - recipientProvider!.selectRecipient(recipient); - _cameFromRecipientList = fromList; - _setPreviousDestination(); - _selected = PayoutDestination.payment; - notifyListeners(); - } else { - debugPrint("RecipientProvider is null — cannot select recipient"); - } + recipientProvider.setCurrentObject(recipient?.id); + _cameFromRecipientList = fromList; + _setPreviousDestination(); + _selected = PayoutDestination.payment; + notifyListeners(); } void editRecipient(Recipient? recipient, {bool fromList = false}) { - if (recipientProvider != null) { - recipientProvider!.selectRecipient(recipient); - _cameFromRecipientList = fromList; - _selected = PayoutDestination.addrecipient; - notifyListeners(); - } else { - debugPrint("RecipientProvider is null — cannot select recipient"); - } + recipientProvider.setCurrentObject(recipient?.id); + _cameFromRecipientList = fromList; + _selected = PayoutDestination.addrecipient; + notifyListeners(); } void goToAddRecipient() { - if (recipientProvider != null) { - AmplitudeService.recipientAddStarted(); - recipientProvider!.selectRecipient(null); - _selected = PayoutDestination.addrecipient; - _cameFromRecipientList = false; - notifyListeners(); - } else { - debugPrint("RecipientProvider is null — cannot go to add recipient"); - } + AmplitudeService.recipientAddStarted(); + recipientProvider!.setCurrentObject(null); + _selected = PayoutDestination.addrecipient; + _cameFromRecipientList = false; + notifyListeners(); } void startPaymentWithoutRecipient(PaymentType type) { - if (recipientProvider != null) { - recipientProvider!.selectRecipient(null); - } + recipientProvider.setCurrentObject(null); _type = type; _cameFromRecipientList = false; _setPreviousDestination(); @@ -111,13 +96,9 @@ class PageSelectorProvider extends ChangeNotifier { } void selectWallet(Wallet wallet) { - if (walletsProvider != null) { - walletsProvider!.selectWallet(wallet); - _selected = PayoutDestination.editwallet; - notifyListeners(); - } else { - debugPrint("WalletsProvider is null — cannot select wallet"); - } + walletsProvider.selectWallet(wallet); + _selected = PayoutDestination.editwallet; + notifyListeners(); } void startPaymentFromWallet(Wallet wallet) { @@ -129,26 +110,26 @@ class PageSelectorProvider extends ChangeNotifier { } PaymentMethod? getPaymentMethodForWallet(Wallet wallet) { - if (methodsProvider == null || methodsProvider!.methods.isEmpty) { + if (methodsProvider.methods.isEmpty) { return null; } return methodsProvider!.methods.firstWhereOrNull( (method) => method.type == PaymentType.wallet && - method.details.contains(wallet.walletUserID) + (method.description?.contains(wallet.walletUserID) ?? false), ); } Map getAvailablePaymentTypes() { final recipient = selectedRecipient; - if (recipient == null) return {}; + if ((recipient == null) || !methodsProvider.isReady) return {}; + + final methodsForRecipient = methodsProvider.methods.where( + (method) => !method.isArchived && method.recipientRef == recipient.id, + ); return { - if (recipient.card != null) PaymentType.card: recipient.card!, - if (recipient.iban != null) PaymentType.iban: recipient.iban!, - if (recipient.wallet != null) PaymentType.wallet: recipient.wallet!, - if (recipient.bank != null) PaymentType.bankAccount: recipient.bank!, - if (recipient.cryptoAddress != null) PaymentType.cryptoAddress: recipient.cryptoAddress!, + for (final method in methodsForRecipient) method.type: method.data, }; } @@ -158,11 +139,11 @@ class PageSelectorProvider extends ChangeNotifier { if (availableTypes.containsKey(currentType)) { return currentType; - } else if (availableTypes.isNotEmpty) { - return availableTypes.keys.first; - } else { - return PaymentType.bankAccount; } + if (availableTypes.isNotEmpty) { + return availableTypes.keys.first; + } + return PaymentType.bankAccount; } bool shouldShowPaymentForm() { @@ -170,11 +151,11 @@ class PageSelectorProvider extends ChangeNotifier { } void handleWalletAutoSelection() { - if (selectedWallet != null && methodsProvider != null) { + if (selectedWallet != null) { final wallet = selectedWallet!; final matchingMethod = getPaymentMethodForWallet(wallet); if (matchingMethod != null) { - methodsProvider!.selectMethod(matchingMethod); + methodsProvider.setCurrentObject(matchingMethod.id); } } } @@ -185,6 +166,6 @@ class PageSelectorProvider extends ChangeNotifier { } } - Recipient? get selectedRecipient => recipientProvider?.selectedRecipient; - Wallet? get selectedWallet => walletsProvider?.selectedWallet; + Recipient? get selectedRecipient => recipientProvider.currentObject; + Wallet? get selectedWallet => walletsProvider.selectedWallet; } diff --git a/frontend/pweb/lib/providers/payment_methods.dart b/frontend/pweb/lib/providers/payment_methods.dart deleted file mode 100644 index 4996469..0000000 --- a/frontend/pweb/lib/providers/payment_methods.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/payment/methods/type.dart'; - -import 'package:pweb/services/payments/payment_methods.dart'; - - -class PaymentMethodsProvider extends ChangeNotifier { - final PaymentMethodsService service; - - List _methods = []; - PaymentMethod? _selectedMethod; - bool _isLoading = false; - String? _error; - - PaymentMethodsProvider({required this.service}); - - List get methods => _methods; - PaymentMethod? get selectedMethod => _selectedMethod; - bool get isLoading => _isLoading; - String? get error => _error; - - Future loadMethods() async { - _isLoading = true; - _error = null; - notifyListeners(); - - try { - _methods = await service.fetchMethods(); - _selectedMethod = _methods.firstWhere((m) => m.isMain, orElse: () => _methods.first); - } catch (e) { - _error = e.toString(); - } - - _isLoading = false; - notifyListeners(); - } - - void selectMethod(PaymentMethod method) { - _selectedMethod = method; - notifyListeners(); - } - - void deleteMethod(PaymentMethod method) { - _methods.remove(method); - notifyListeners(); - } - - void reorderMethods(int oldIndex, int newIndex) { - if (newIndex > oldIndex) newIndex--; - final item = _methods.removeAt(oldIndex); - _methods.insert(newIndex, item); - notifyListeners(); - } - - void toggleEnabled(PaymentMethod method, bool value) { - method.isEnabled = value; - notifyListeners(); - } - - void makeMain(PaymentMethod method) { - for (final m in _methods) { - m.isMain = false; - } - method.isMain = true; - selectMethod(method); - } -} diff --git a/frontend/pweb/lib/providers/upload_history.dart b/frontend/pweb/lib/providers/upload_history.dart index 02a8325..ab5b4ff 100644 --- a/frontend/pweb/lib/providers/upload_history.dart +++ b/frontend/pweb/lib/providers/upload_history.dart @@ -1,10 +1,9 @@ import 'package:pshared/models/payment/upload_history_item.dart'; import 'package:pweb/providers/template.dart'; -import 'package:pweb/services/payments/upload_history.dart'; +import 'package:pweb/services/payments/history.dart'; class UploadHistoryProvider extends FutureProviderTemplate> { - UploadHistoryProvider({required UploadHistoryService service}) - : super(loader: service.fetchHistory); + UploadHistoryProvider({required UploadHistoryService service}) : super(loader: service.fetchHistory); } \ No newline at end of file diff --git a/frontend/pweb/lib/services/payments/upload_history.dart b/frontend/pweb/lib/services/payments/history.dart similarity index 100% rename from frontend/pweb/lib/services/payments/upload_history.dart rename to frontend/pweb/lib/services/payments/history.dart diff --git a/frontend/pweb/lib/services/payments/methods.dart b/frontend/pweb/lib/services/payments/methods.dart new file mode 100644 index 0000000..7a33d49 --- /dev/null +++ b/frontend/pweb/lib/services/payments/methods.dart @@ -0,0 +1,48 @@ +// import 'package:pshared/models/payment/methods/type.dart'; + +// import 'package:pshared/models/payment/type.dart'; + + +// abstract class PaymentMethodsService { +// Future> fetchMethods(); +// } + +// class MockPaymentMethodsService implements PaymentMethodsService { +// @override +// Future> fetchMethods() async { +// await Future.delayed(const Duration(milliseconds: 200)); +// return [ +// PaymentMethod( +// id: '1', +// label: 'My account', +// details: '•••4567', +// type: PaymentType.bankAccount, +// isMain: true, +// ), +// PaymentMethod( +// id: '2', +// label: 'Euro IBAN', +// details: 'DE•• •••8901', +// type: PaymentType.iban, +// ), +// PaymentMethod( +// id: '3', +// label: 'Wallet', +// details: 'WA‑12345667', +// type: PaymentType.wallet, +// ), +// PaymentMethod( +// id: '4', +// label: 'Wallet', +// details: 'WA-76654321', +// type: PaymentType.wallet, +// ), +// PaymentMethod( +// id: '5', +// label: 'Credit Card', +// details: '21•• •••• •••• 8901', +// type: PaymentType.card, +// ), +// ]; +// } +// } diff --git a/frontend/pweb/lib/services/payments/payment_methods.dart b/frontend/pweb/lib/services/payments/payment_methods.dart deleted file mode 100644 index 7886ac5..0000000 --- a/frontend/pweb/lib/services/payments/payment_methods.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:pshared/models/payment/methods/type.dart'; - -import 'package:pshared/models/payment/type.dart'; - - -abstract class PaymentMethodsService { - Future> fetchMethods(); -} - -class MockPaymentMethodsService implements PaymentMethodsService { - @override - Future> fetchMethods() async { - await Future.delayed(const Duration(milliseconds: 200)); - return [ - PaymentMethod( - id: '1', - label: 'My account', - details: '•••4567', - type: PaymentType.bankAccount, - isMain: true, - ), - PaymentMethod( - id: '2', - label: 'Euro IBAN', - details: 'DE•• •••8901', - type: PaymentType.iban, - ), - PaymentMethod( - id: '3', - label: 'Wallet', - details: 'WA‑12345667', - type: PaymentType.wallet, - ), - PaymentMethod( - id: '4', - label: 'Wallet', - details: 'WA-76654321', - type: PaymentType.wallet, - ), - PaymentMethod( - id: '5', - label: 'Credit Card', - details: '21•• •••• •••• 8901', - type: PaymentType.card, - ), - ]; - } -} diff --git a/frontend/pweb/lib/services/recipient/recipient.dart b/frontend/pweb/lib/services/recipient/recipient.dart index 7c4cc68..0b114cc 100644 --- a/frontend/pweb/lib/services/recipient/recipient.dart +++ b/frontend/pweb/lib/services/recipient/recipient.dart @@ -1,88 +1,88 @@ -import 'package:pshared/models/recipient/recipient.dart'; -import 'package:pshared/models/payment/methods/card.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/recipient/status.dart'; -import 'package:pshared/models/recipient/type.dart'; +// import 'package:pshared/models/recipient/recipient.dart'; +// import 'package:pshared/models/payment/methods/card.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/recipient/status.dart'; +// import 'package:pshared/models/recipient/type.dart'; -class RecipientService { - Future> fetchRecipients() async { - await Future.delayed(const Duration(milliseconds: 500)); - return RecipientMockData.all; - } -} +// class RecipientService { +// Future> fetchRecipients() async { +// await Future.delayed(const Duration(milliseconds: 500)); +// return RecipientMockData.all; +// } +// } -class RecipientMockData { - static List get all => [ - Recipient.mock( - name: 'Alice Johnson', - email: 'alice@example.com', - status: RecipientStatus.ready, - type: RecipientType.internal, - card: CardPaymentMethod( - pan: '1213', - firstName: 'Alice', - lastName: 'Johnson', - ), - ), - Recipient.mock( - name: 'Bob & Co Ltd.', - email: 'payout@bobco.com', - status: RecipientStatus.registered, - type: RecipientType.external, - card: CardPaymentMethod( - pan: '4343', - firstName: 'Bob', - lastName: 'Co', - ), - iban: IbanPaymentMethod( - iban: 'FR7630***890189', - accountHolder: 'Bob & Co Ltd.', - bic: 'AGRIFRPP', - bankName: 'Credit Agricole', - ), - wallet: WalletPaymentMethod(walletId: '8932231'), - ), - Recipient.mock( - name: 'Carlos Kline', - email: 'carlos@acme.org', - status: RecipientStatus.notRegistered, - type: RecipientType.internal, - wallet: WalletPaymentMethod(walletId: '7723490'), - ), - Recipient.mock( - name: 'Delta Outsourcing GmbH', - email: 'finance@delta-os.de', - status: RecipientStatus.registered, - type: RecipientType.external, - card: CardPaymentMethod( - pan: '9988', - firstName: 'Delta', - lastName: 'GmbH', - ), - iban: IbanPaymentMethod( - iban: 'DE4450***324931', - accountHolder: 'Delta Outsourcing GmbH', - bic: 'INGDDEFFXXX', - bankName: 'ING', - ), - ), - Recipient.mock( - name: 'Erin Patel', - email: 'erin@labster.io', - status: RecipientStatus.ready, - type: RecipientType.internal, - bank: RussianBankAccountPaymentMethod( - accountNumber: '4081***7654', - recipientName: 'Erin Patel', - inn: '7812012345', - kpp: '781201001', - bankName: 'Alfa-Bank', - bik: '044525593', - correspondentAccount: '30101810200000000593', - ), - ), - ]; -} +// class RecipientMockData { +// static List get all => [ +// Recipient.mock( +// name: 'Alice Johnson', +// email: 'alice@example.com', +// status: RecipientStatus.ready, +// type: RecipientType.internal, +// card: CardPaymentMethod( +// pan: '1213', +// firstName: 'Alice', +// lastName: 'Johnson', +// ), +// ), +// Recipient.mock( +// name: 'Bob & Co Ltd.', +// email: 'payout@bobco.com', +// status: RecipientStatus.registered, +// type: RecipientType.external, +// card: CardPaymentMethod( +// pan: '4343', +// firstName: 'Bob', +// lastName: 'Co', +// ), +// iban: IbanPaymentMethod( +// iban: 'FR7630***890189', +// accountHolder: 'Bob & Co Ltd.', +// bic: 'AGRIFRPP', +// bankName: 'Credit Agricole', +// ), +// wallet: WalletPaymentMethod(walletId: '8932231'), +// ), +// Recipient.mock( +// name: 'Carlos Kline', +// email: 'carlos@acme.org', +// status: RecipientStatus.notRegistered, +// type: RecipientType.internal, +// wallet: WalletPaymentMethod(walletId: '7723490'), +// ), +// Recipient.mock( +// name: 'Delta Outsourcing GmbH', +// email: 'finance@delta-os.de', +// status: RecipientStatus.registered, +// type: RecipientType.external, +// card: CardPaymentMethod( +// pan: '9988', +// firstName: 'Delta', +// lastName: 'GmbH', +// ), +// iban: IbanPaymentMethod( +// iban: 'DE4450***324931', +// accountHolder: 'Delta Outsourcing GmbH', +// bic: 'INGDDEFFXXX', +// bankName: 'ING', +// ), +// ), +// Recipient.mock( +// name: 'Erin Patel', +// email: 'erin@labster.io', +// status: RecipientStatus.ready, +// type: RecipientType.internal, +// bank: RussianBankAccountPaymentMethod( +// accountNumber: '4081***7654', +// recipientName: 'Erin Patel', +// inn: '7812012345', +// kpp: '781201001', +// bankName: 'Alfa-Bank', +// bik: '044525593', +// correspondentAccount: '30101810200000000593', +// ), +// ), +// ]; +// } diff --git a/frontend/pweb/lib/utils/payment/dropdown.dart b/frontend/pweb/lib/utils/payment/dropdown.dart index 0358db0..7b9b292 100644 --- a/frontend/pweb/lib/utils/payment/dropdown.dart +++ b/frontend/pweb/lib/utils/payment/dropdown.dart @@ -48,7 +48,7 @@ class _PaymentMethodDropdownState extends State { children: [ Icon(iconForPaymentType(method.type), size: 20), const SizedBox(width: 8), - Text('${method.label} (${method.details})'), + Text('${method.name}' + (method.description == null ? '' : ' (${method.description!})')), ], ), ); diff --git a/frontend/pweb/lib/utils/payment/label.dart b/frontend/pweb/lib/utils/payment/label.dart index 75aea00..38cacd7 100644 --- a/frontend/pweb/lib/utils/payment/label.dart +++ b/frontend/pweb/lib/utils/payment/label.dart @@ -12,6 +12,6 @@ String getPaymentTypeLabel(BuildContext context, PaymentType type) { PaymentType.bankAccount => l10n.paymentTypeBankAccount, PaymentType.iban => l10n.paymentTypeIban, PaymentType.wallet => l10n.paymentTypeWallet, - PaymentType.cryptoAddress => 'Crypto address', + PaymentType.cryptoAddress => l10n.paymentTypeCryptoAddress, }; } diff --git a/frontend/pweb/lib/widgets/sidebar/page.dart b/frontend/pweb/lib/widgets/sidebar/page.dart index fdba1e7..2df19e1 100644 --- a/frontend/pweb/lib/widgets/sidebar/page.dart +++ b/frontend/pweb/lib/widgets/sidebar/page.dart @@ -72,7 +72,7 @@ class PageSelector extends StatelessWidget { break; case PayoutDestination.addrecipient: - final recipient = provider.recipientProvider?.selectedRecipient; + final recipient = provider.recipientProvider.currentObject; content = AdressBookRecipientForm( recipient: recipient, onSaved: (_) => provider.selectPage(PayoutDestination.recipients), @@ -100,7 +100,7 @@ class PageSelector extends StatelessWidget { break; case PayoutDestination.editwallet: - final wallet = provider.walletsProvider?.selectedWallet; + final wallet = provider.walletsProvider.selectedWallet; content = wallet != null ? WalletEditPage( onBack: provider.goBackFromWalletEdit,