temp build

This commit is contained in:
Stephan D
2025-12-05 01:32:41 +01:00
parent 082d782a80
commit f71cc76f64
50 changed files with 853 additions and 707 deletions

View File

@@ -13,6 +13,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
mntxservice "github.com/tech/sendico/gateway/mntx/internal/service/gateway" 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/api/routers"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
msg "github.com/tech/sendico/pkg/messaging" msg "github.com/tech/sendico/pkg/messaging"
@@ -168,7 +169,7 @@ func (i *Imp) loadConfig() (*config, error) {
return cfg, nil 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) baseURL := strings.TrimSpace(cfg.BaseURL)
if env := strings.TrimSpace(cfg.BaseURLEnv); env != "" { if env := strings.TrimSpace(cfg.BaseURLEnv); env != "" {
if val := strings.TrimSpace(os.Getenv(env)); val != "" { 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 { if id, err := strconv.ParseInt(raw, 10, 64); err == nil {
projectID = id projectID = id
} else { } 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) statusSuccess := strings.TrimSpace(cfg.StatusSuccess)
statusProcessing := strings.TrimSpace(cfg.StatusProcessing) statusProcessing := strings.TrimSpace(cfg.StatusProcessing)
return mntxservice.MonetixConfig{ return monetix.Config{
BaseURL: baseURL, BaseURL: baseURL,
ProjectID: projectID, ProjectID: projectID,
SecretKey: secret, SecretKey: secret,

View File

@@ -8,7 +8,6 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -31,7 +30,7 @@ func NewClient(cfg Config, httpClient *http.Client, logger mlogger.Logger) *Clie
return &Client{ return &Client{
cfg: cfg, cfg: cfg,
client: client, client: client,
logger: cl.Named("monetix_client"), logger: cl.Named("client"),
} }
} }

View File

@@ -64,8 +64,10 @@ func (t *PaymentType) UnmarshalJSON(data []byte) error {
type PaymentMethod struct { type PaymentMethod struct {
PermissionBound `bson:",inline" json:",inline"` PermissionBound `bson:",inline" json:",inline"`
Describable `bson:",inline" json:",inline"`
RecipientRef primitive.ObjectID `bson:"recipientRef" json:"recipientRef"` RecipientRef primitive.ObjectID `bson:"recipientRef" json:"recipientRef"`
Type PaymentType `bson:"type" json:"type"` Type PaymentType `bson:"type" json:"type"`
Data bson.Raw `bson:"data" json:"data"` Data bson.Raw `bson:"data" json:"data"`
IsMain bool `bson:"isMain" json:"isMain"`
} }

View File

@@ -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<PaymentMethodDTO> paymentMethods;
const PaymentMethodResponse({required super.accessToken, required this.paymentMethods});
factory PaymentMethodResponse.fromJson(Map<String, dynamic> json) => _$PaymentMethodResponseFromJson(json);
@override
Map<String, dynamic> toJson() => _$PaymentMethodResponseToJson(this);
}

View File

@@ -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<RecipientDTO> recipients;
const RecipientResponse({required super.accessToken, required this.recipients});
factory RecipientResponse.fromJson(Map<String, dynamic> json) => _$RecipientResponseFromJson(json);
@override
Map<String, dynamic> toJson() => _$RecipientResponseToJson(this);
}

View File

@@ -10,6 +10,9 @@ part 'method.g.dart';
class PaymentMethodDTO extends PermissionBoundDTO { class PaymentMethodDTO extends PermissionBoundDTO {
final String recipientRef; final String recipientRef;
final String type; final String type;
final String name;
final String? description;
final bool isMain;
final Map<String, dynamic> data; final Map<String, dynamic> data;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
@@ -24,6 +27,9 @@ class PaymentMethodDTO extends PermissionBoundDTO {
required this.recipientRef, required this.recipientRef,
required this.type, required this.type,
required this.data, required this.data,
required this.name,
required this.isMain,
this.description,
this.isArchived = false, this.isArchived = false,
}); });

View File

@@ -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/russian_bank.dart';
import 'package:pshared/data/mapper/payment/type.dart'; import 'package:pshared/data/mapper/payment/type.dart';
import 'package:pshared/data/mapper/payment/wallet.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/organization/bound.dart';
import 'package:pshared/models/payment/methods/card.dart'; import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/crypto_address.dart'; import 'package:pshared/models/payment/methods/crypto_address.dart';
import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/iban.dart'; import 'package:pshared/models/payment/methods/iban.dart';
import 'package:pshared/models/payment/methods/russian_bank.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/methods/wallet.dart';
import 'package:pshared/models/payment/payment_method.dart';
import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/permissions/bound.dart'; import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
extension PaymentMethodModelMapper on PaymentMethodModel { extension PaymentMethodMapper on PaymentMethod {
PaymentMethodDTO toDTO() => PaymentMethodDTO( PaymentMethodDTO toDTO() => PaymentMethodDTO(
id: storable.id, id: storable.id,
createdAt: storable.createdAt, createdAt: storable.createdAt,
@@ -34,6 +35,9 @@ extension PaymentMethodModelMapper on PaymentMethodModel {
type: paymentTypeToValue(type), type: paymentTypeToValue(type),
data: _dataToJson(data), data: _dataToJson(data),
isArchived: isArchived, isArchived: isArchived,
name: describable.name,
description: describable.description,
isMain: isMain,
); );
Map<String, dynamic> _dataToJson(PaymentMethodData data) { Map<String, dynamic> _dataToJson(PaymentMethodData data) {
@@ -53,7 +57,7 @@ extension PaymentMethodModelMapper on PaymentMethodModel {
} }
extension PaymentMethodDTOMapper on PaymentMethodDTO { extension PaymentMethodDTOMapper on PaymentMethodDTO {
PaymentMethodModel toDomain() => PaymentMethodModel( PaymentMethod toDomain() => PaymentMethod(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
permissionBound: newPermissionBound( permissionBound: newPermissionBound(
organizationBound: newOrganizationBound(organizationRef: organizationRef), organizationBound: newOrganizationBound(organizationRef: organizationRef),
@@ -62,6 +66,8 @@ extension PaymentMethodDTOMapper on PaymentMethodDTO {
recipientRef: recipientRef, recipientRef: recipientRef,
data: _dataToDomain(paymentTypeFromValue(type), data), data: _dataToDomain(paymentTypeFromValue(type), data),
isArchived: isArchived, isArchived: isArchived,
describable: newDescribable(name: name, description: description),
isMain: isMain,
); );
PaymentMethodData _dataToDomain(PaymentType paymentType, Map<String, dynamic> payload) { PaymentMethodData _dataToDomain(PaymentType paymentType, Map<String, dynamic> payload) {

View File

@@ -2,13 +2,13 @@ import 'package:pshared/data/dto/recipient/recipient.dart';
import 'package:pshared/models/describable.dart'; import 'package:pshared/models/describable.dart';
import 'package:pshared/models/organization/bound.dart'; import 'package:pshared/models/organization/bound.dart';
import 'package:pshared/models/permissions/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/status.dart';
import 'package:pshared/models/recipient/type.dart'; import 'package:pshared/models/recipient/type.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
extension RecipientModelMapper on RecipientModel { extension RecipientModelMapper on Recipient {
RecipientDTO toDTO() => RecipientDTO( RecipientDTO toDTO() => RecipientDTO(
id: storable.id, id: storable.id,
createdAt: storable.createdAt, createdAt: storable.createdAt,
@@ -26,7 +26,7 @@ extension RecipientModelMapper on RecipientModel {
} }
extension RecipientDTOMapper on RecipientDTO { extension RecipientDTOMapper on RecipientDTO {
RecipientModel toDomain() => RecipientModel( Recipient toDomain() => Recipient(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
permissionBound: newPermissionBound( permissionBound: newPermissionBound(
organizationBound: newOrganizationBound(organizationRef: organizationRef), organizationBound: newOrganizationBound(organizationRef: organizationRef),

View File

@@ -19,5 +19,15 @@
"operationStatusError": "Error", "operationStatusError": "Error",
"@operationStatusError": { "@operationStatusError": {
"description": "Label for the “error” operation status" "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"
} }
} }

View File

@@ -19,5 +19,15 @@
"operationStatusError": "Ошибка", "operationStatusError": "Ошибка",
"@operationStatusError": { "@operationStatusError": {
"description": "Label for the “error” operation status" "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"
} }
} }

View File

@@ -1,21 +1,62 @@
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/payment/type.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({ class PaymentMethod implements PermissionBoundStorable, Describable {
required this.id, final Storable storable;
required this.label, final PermissionBound permissionBound;
required this.details, final Describable describable;
required this.type, final String recipientRef;
this.isEnabled = true, 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, this.isMain = false,
}); });
final String id; PaymentType get type => data.type;
final String label;
final String details;
final PaymentType type;
bool isEnabled; @override
bool isMain; 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,
);
}

View File

@@ -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,
);
}

View File

@@ -1,86 +1,97 @@
import 'package:pshared/models/payment/methods/card.dart'; import 'package:pshared/models/describable.dart';
import 'package:pshared/models/payment/methods/iban.dart'; import 'package:pshared/models/organization/bound.dart';
import 'package:pshared/models/payment/methods/crypto_address.dart'; import 'package:pshared/models/permissions/bound/describable.dart';
import 'package:pshared/models/payment/methods/russian_bank.dart'; import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/payment/methods/wallet.dart';
import 'package:pshared/models/recipient/status.dart'; import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/models/recipient/type.dart'; import 'package:pshared/models/recipient/type.dart';
import 'package:pshared/models/storable.dart';
class Recipient { class Recipient implements PermissionBoundStorableDescribable {
final String? avatarUrl; // network URL / local asset final Storable storable;
final String name; final PermissionBound permissionBound;
final Describable describable;
final String email; final String email;
final String? avatarUrl;
final RecipientStatus status; final RecipientStatus status;
final RecipientType type; final RecipientType type;
final CardPaymentMethod? card; final bool isArchived;
final IbanPaymentMethod? iban;
final RussianBankAccountPaymentMethod? bank;
final WalletPaymentMethod? wallet;
final CryptoAddressPaymentMethod? cryptoAddress;
const Recipient({ const Recipient({
this.avatarUrl, required this.storable,
required this.name, required this.permissionBound,
required this.describable,
required this.email, required this.email,
required this.status, required this.status,
required this.type, required this.type,
this.card, this.avatarUrl,
this.iban, this.isArchived = false,
this.bank,
this.wallet,
this.cryptoAddress,
}); });
/// Convenience factory for quickly creating mock recipients. @override
factory Recipient.mock({ String get id => storable.id;
required String name, @override
required String email, DateTime get createdAt => storable.createdAt;
required RecipientStatus status, @override
required RecipientType type, DateTime get updatedAt => storable.updatedAt;
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 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) { bool matchesQuery(String q) {
final searchable = [ final searchable = [
name, name,
email, 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,
);

View File

@@ -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,
);
}

View File

@@ -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<PaymentMethod> {
late OrganizationsProvider _organizations;
late RecipientsProvider _recipients;
PaymentMethodsProvider() : super(service: PaymentMethodService.basicService);
List<PaymentMethod> get methods => List<PaymentMethod>.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt)));
void updateProviders(OrganizationsProvider organizations, RecipientsProvider recipients) {
_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<void> updateMethod(PaymentMethod method) async => update(method.toDTO().toJson());
Future<void> setArchivedMethod({
required PaymentMethod method,
required bool newIsArchived,
}) async => setArchived(
organizationRef: _organizations.current.id,
objectRef: method.id,
newIsArchived: newIsArchived,
cascade: true,
);
Future<void> makeMain(PaymentMethod method) {
// TODO: create separate backend method to manage main payment method
final updates = <Future<void>>[];
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);
}
}

View File

@@ -1,34 +1,27 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/models/recipient/filter.dart'; import 'package:pshared/models/recipient/filter.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/recipient/status.dart'; import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pweb/services/recipient/recipient.dart'; import 'package:pshared/provider/template.dart';
import 'package:pshared/service/recipient/service.dart';
class RecipientProvider extends ChangeNotifier { class RecipientsProvider extends GenericProvider<Recipient> {
final RecipientService _service; late OrganizationsProvider _organizations;
RecipientProvider(this._service);
List<Recipient> _recipients = [];
bool _isLoading = false;
String? _error;
RecipientFilter _selectedFilter = RecipientFilter.all; RecipientFilter _selectedFilter = RecipientFilter.all;
String _query = ''; String _query = '';
Recipient? _selectedRecipient;
List<Recipient> get recipients => _recipients;
bool get isLoading => _isLoading;
String? get error => _error;
RecipientFilter get selectedFilter => _selectedFilter; RecipientFilter get selectedFilter => _selectedFilter;
String get query => _query; String get query => _query;
Recipient? get selectedRecipient => _selectedRecipient;
List<Recipient> get recipients => List<Recipient>.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt)));
RecipientsProvider() : super(service: RecipientService.basicService);
List<Recipient> get filteredRecipients { List<Recipient> get filteredRecipients {
List<Recipient> filtered = _recipients.where((r) { List<Recipient> filtered = recipients.where((r) {
switch (_selectedFilter) { switch (_selectedFilter) {
case RecipientFilter.ready: case RecipientFilter.ready:
return r.status == RecipientStatus.ready; return r.status == RecipientStatus.ready;
@@ -48,21 +41,6 @@ class RecipientProvider extends ChangeNotifier {
return filtered; return filtered;
} }
Future<void> 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) { void setFilter(RecipientFilter filter) {
_selectedFilter = filter; _selectedFilter = filter;
notifyListeners(); notifyListeners();
@@ -73,8 +51,10 @@ class RecipientProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void selectRecipient(Recipient? recipient) { void updateProviders(OrganizationsProvider organizations) {
_selectedRecipient = recipient; _organizations = organizations;
notifyListeners(); if (_organizations.isOrganizationSet) {
load(_organizations.current.id, _organizations.current.id);
}
} }
} }

View File

@@ -32,21 +32,23 @@ List<T> mergeLists<T>({
/// to manage state (loading, error, data) without reimplementing service logic. /// to manage state (loading, error, data) without reimplementing service logic.
class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier { class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier {
final BasicService<T> service; final BasicService<T> service;
bool _isLoaded = false;
Resource<List<T>> _resource = Resource(data: []); Resource<List<T>> _resource = Resource(data: []);
Resource<List<T>> get resource => _resource; Resource<List<T>> get resource => _resource;
List<T> get items => List.unmodifiable(_resource.data ?? []); List<T> get items => List.unmodifiable(_resource.data ?? []);
bool get isLoading => _resource.isLoading; bool get isLoading => _resource.isLoading;
bool get isEmpty => items.isEmpty;
Object? get error => _resource.error; Object? get error => _resource.error;
bool get isReady => (error == null) && _isLoaded;
bool get isCurrentSet => _currentObjectRef != null;
String? _currentObjectRef; // Stores the currently selected project ref String? _currentObjectRef; // Stores the currently selected project ref
T? get currentObject => _resource.data?.firstWhereOrNull( T? get currentObject => _resource.data?.firstWhereOrNull(
(object) => object.id == _currentObjectRef, (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}); GenericProvider({required this.service});
@@ -67,11 +69,13 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
notifyListeners(); notifyListeners();
} }
Future<void> loadFuture(Future<List<T>> future) async { Future<List<T>> loadFuture(Future<List<T>> future) async {
_setResource(_resource.copyWith(isLoading: true)); _setResource(_resource.copyWith(isLoading: true));
try { try {
final list = await future; final list = await future;
_isLoaded = true;
_setResource(Resource(data: list, isLoading: false)); _setResource(Resource(data: list, isLoading: false));
return list;
} catch (e) { } catch (e) {
_setResource( _setResource(
_resource.copyWith(isLoading: false, error: toException(e)), _resource.copyWith(isLoading: false, error: toException(e)),
@@ -80,17 +84,30 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
} }
} }
Future<void> load(String organizationRef, String? parentRef) async { Future<void> load(
String organizationRef,
String? parentRef, {
int? limit,
int? offset,
bool? Function()? fetchArchived,
}) async {
if (parentRef != null) { 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<void> loadItem(String itemRef) async { Future<void> loadItem(String itemRef) async {
return loadFuture((() async => [await service.get(itemRef)])()); await loadFuture((() async => [await service.get(itemRef)])());
} }
List<T> merge(List<T> rhs) => mergeLists<T>( List<T> merge(List<T> rhs) => mergeLists<T>(
lhs: items, lhs: items,
rhs: rhs, rhs: rhs,
@@ -134,11 +151,47 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
} }
} }
Future<void> delete(String objectRef) async { Future<void> delete(String objectRef, {Map<String, dynamic>? request}) async {
_setResource(_resource.copyWith(isLoading: true)); _setResource(_resource.copyWith(isLoading: true));
try { 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<void> toggleArchived(T item, bool currentState, {bool? cascade}) => setArchived(
organizationRef: item.organizationRef,
objectRef: item.id,
newIsArchived: !currentState,
cascade: cascade ?? true,
);
Future<void> 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) { if (_currentObjectRef == objectRef) {
_currentObjectRef = null; _currentObjectRef = null;
} }
@@ -154,11 +207,17 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
} }
bool setCurrentObject(String? objectRef) { bool setCurrentObject(String? objectRef) {
if (_currentObjectRef == objectRef) {
// No change, skip notification
return true;
}
if (objectRef == null) { if (objectRef == null) {
_currentObjectRef = null; _currentObjectRef = null;
notifyListeners(); notifyListeners();
return true; return true;
} }
if (_resource.data?.any((p) => p.id == objectRef) ?? false) { if (_resource.data?.any((p) => p.id == objectRef) ?? false) {
_currentObjectRef = objectRef; _currentObjectRef = objectRef;
notifyListeners(); notifyListeners();
@@ -167,4 +226,5 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
return false; // Object not found return false; // Object not found
} }
} }

View File

@@ -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<PaymentMethod> _basicService = BasicService<PaymentMethod>(
objectType: _objectType,
fromJson: (json) => PaymentMethodResponse.fromJson(json).paymentMethods.map((dto) => dto.toDomain()).toList(),
);
static BasicService<PaymentMethod> get basicService => _basicService;
static Future<List<PaymentMethod>> list(String organizationRef, String recipientRef) async {
return _basicService.list(organizationRef, recipientRef);
}
static Future<PaymentMethod> get(String recipientRef) async {
return _basicService.get(recipientRef);
}
static Future<List<PaymentMethod>> create(String organizationRef, PaymentMethod paymentMethod) async {
return _basicService.create(organizationRef, paymentMethod.toDTO().toJson());
}
static Future<List<PaymentMethod>> update(PaymentMethod paymentMethod) async {
return _basicService.update(paymentMethod.toDTO().toJson());
}
static Future<List<PaymentMethod>> delete(PaymentMethod paymentMethod) async {
return _basicService.delete(paymentMethod.storable.id);
}
}

View File

@@ -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<Recipient> _basicService = BasicService<Recipient>(
objectType: _objectType,
fromJson: (json) => RecipientResponse.fromJson(json).recipients.map((dto) => dto.toDomain()).toList(),
);
static BasicService<Recipient> get basicService => _basicService;
static Future<List<Recipient>> list(String organizationRef, String _) async {
return _basicService.list(organizationRef, organizationRef);
}
static Future<Recipient> get(String recipientRef) async {
return _basicService.get(recipientRef);
}
static Future<List<Recipient>> create(String organizationRef, Recipient recipient) async {
return _basicService.create(organizationRef, recipient.toDTO().toJson());
}
static Future<List<Recipient>> update(Recipient recipient) async {
return _basicService.update(recipient.toDTO().toJson());
}
static Future<List<Recipient>> delete(Recipient recipient) async {
return _basicService.delete(recipient.storable.id);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/utils/http/params.dart';
class BasicService<T> { class BasicService<T> {
@@ -15,15 +16,26 @@ class BasicService<T> {
required this.fromJson, required this.fromJson,
}) : _objectType = objectType, _logger = Logger('service.$objectType'); }) : _objectType = objectType, _logger = Logger('service.$objectType');
Future<List<T>> list(String organizationRef, String parentRef) async { String _refLog(String ref) => ref.isEmpty ? '<not set>' : ref;
_logger.fine('Loading all objects');
Future<List<T>> 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( return _getObjects(
AuthorizationService.getGETResponse(_objectType, '/list/$organizationRef/$parentRef'), AuthorizationService.getGETResponse(
_objectType,
paramsToUriString(
path: '/list/$organizationRef/$parentRef',
limit: limit,
offset: offset,
fetchArchived: fetchArchived,
),
),
); );
} }
Future<T> get(String objectRef) async { Future<T> get(String objectRef) async {
_logger.fine('Loading object $objectRef'); _logger.fine('Loading $_objectType $objectRef');
final objects = await _getObjects( final objects = await _getObjects(
AuthorizationService.getGETResponse(_objectType, '/$objectRef'), AuthorizationService.getGETResponse(_objectType, '/$objectRef'),
); );
@@ -31,24 +43,36 @@ class BasicService<T> {
} }
Future<List<T>> create(String organizationRef, Map<String, dynamic> request) async { Future<List<T>> create(String organizationRef, Map<String, dynamic> request) async {
_logger.fine('Creating new object...'); _logger.fine('Creating new...');
return _getObjects( return _getObjects(
AuthorizationService.getPOSTResponse(_objectType, '/$organizationRef', request), AuthorizationService.getPOSTResponse(_objectType, '/$organizationRef', request),
); );
} }
Future<List<T>> update(Map<String, dynamic> request) async { Future<List<T>> update(Map<String, dynamic> request) async {
_logger.fine('Patching object...'); _logger.fine('Patching...');
return _getObjects( return _getObjects(
AuthorizationService.getPUTResponse(_objectType, '/', request, AuthorizationService.getPUTResponse(_objectType, '/', request,
), ),
); );
} }
Future<List<T>> delete(String objecRef) async { Future<List<T>> delete(String objecRef, {Map<String, dynamic>? request}) async {
_logger.fine('Deleting object $objecRef'); _logger.fine('Deleting $_objectType $objecRef');
return _getObjects( return _getObjects(
AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', {}), AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', request ?? {}),
);
}
Future<List<T>> 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<T> {
_logger.fine('Fetched ${objects.length} object(s)'); _logger.fine('Fetched ${objects.length} object(s)');
return objects; return objects;
} catch (e, stackTrace) { } catch (e, stackTrace) {
_logger.severe('Failed to fetch objects', e, stackTrace); _logger.severe('Failed to fetch', e, stackTrace);
rethrow; rethrow;
} }
} }
String _formatParameter(dynamic value) {
return value?.toString() ?? '<not specified>';
}
} }

View File

@@ -0,0 +1,38 @@
// Query parameter constants
const String _limitParam = 'limit';
const String _offsetParam = 'offset';
const String _archivedParam = 'archived';
void _addIfNotNull(Map<String, String> params, String key, dynamic value) {
if (value != null) {
params[key] = value.toString();
}
}
Uri paramsToUri({
required String path,
int? limit,
int? offset,
bool? fetchArchived,
Map<String, String>? params,
}) {
Map<String, String> 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<String, String> queryParams = const {},
int? limit,
int? offset,
bool? fetchArchived,
}) => paramsToUri(path: path, limit: limit, offset: offset, fetchArchived: fetchArchived).toString();

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/generated/i18n/ps_localizations.dart';
import 'package:pshared/provider/template.dart'; import 'package:pshared/provider/template.dart';
@@ -20,9 +21,10 @@ class ResourceContainer<T extends GenericProvider> extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Consumer<T>(builder: (context, provider, _) { Widget build(BuildContext context) => Consumer<T>(builder: (context, provider, _) {
if (provider.isLoading) return loading ?? Center(child: CircularProgressIndicator()); final l10n = PSLocalizations.of(context)!;
if (provider.error != null) return error ?? Text('Error while loading data. Try again'); if (provider.isLoading) return loading ?? const Center(child: CircularProgressIndicator());
if (provider.isEmpty) return empty ?? Text('Empty data'); if (provider.error != null) return error ?? Text(l10n.resourceLoadError);
if (provider.items.isEmpty) return empty ?? Text(l10n.resourceEmpty);
return builder(context, provider); return builder(context, provider);
}); });
} }

View File

@@ -311,6 +311,7 @@
"paymentTypeBankAccount": "Russian Bank Account", "paymentTypeBankAccount": "Russian Bank Account",
"paymentTypeIban": "IBAN", "paymentTypeIban": "IBAN",
"paymentTypeWallet": "Wallet", "paymentTypeWallet": "Wallet",
"paymentTypeCryptoAddress": "Crypto address",
"cardNumber": "Card Number", "cardNumber": "Card Number",
"enterCardNumber": "Enter the card number", "enterCardNumber": "Enter the card number",

View File

@@ -311,6 +311,7 @@
"paymentTypeBankAccount": "Российский банковский счет", "paymentTypeBankAccount": "Российский банковский счет",
"paymentTypeIban": "IBAN", "paymentTypeIban": "IBAN",
"paymentTypeWallet": "Кошелек", "paymentTypeWallet": "Кошелек",
"paymentTypeCryptoAddress": "Крипто-адрес",
"cardNumber": "Номер карты", "cardNumber": "Номер карты",
"enterCardNumber": "Введите номер карты", "enterCardNumber": "Введите номер карты",

View File

@@ -12,6 +12,8 @@ import 'package:pshared/provider/locale.dart';
import 'package:pshared/provider/permissions.dart'; import 'package:pshared/provider/permissions.dart';
import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/organizations.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/app.dart';
import 'package:pweb/app/timeago.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/mock_payment.dart';
import 'package:pweb/providers/operatioins.dart'; import 'package:pweb/providers/operatioins.dart';
import 'package:pweb/providers/page_selector.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/two_factor.dart';
import 'package:pweb/providers/upload_history.dart'; import 'package:pweb/providers/upload_history.dart';
import 'package:pweb/providers/wallets.dart'; import 'package:pweb/providers/wallets.dart';
import 'package:pweb/providers/wallet_transactions.dart'; import 'package:pweb/providers/wallet_transactions.dart';
// import 'package:pweb/services/amplitude.dart'; // import 'package:pweb/services/amplitude.dart';
import 'package:pweb/services/operations.dart'; import 'package:pweb/services/operations.dart';
import 'package:pweb/services/payments/payment_methods.dart'; import 'package:pweb/services/payments/history.dart';
import 'package:pweb/services/payments/upload_history.dart';
import 'package:pweb/services/recipient/recipient.dart';
import 'package:pweb/services/wallet_transactions.dart'; import 'package:pweb/services/wallet_transactions.dart';
import 'package:pweb/services/wallets.dart'; import 'package:pweb/services/wallets.dart';
@@ -77,8 +75,13 @@ void main() async {
ChangeNotifierProvider( ChangeNotifierProvider(
create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(), create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(),
), ),
ChangeNotifierProvider( ChangeNotifierProxyProvider<OrganizationsProvider, RecipientsProvider>(
create: (_) => PaymentMethodsProvider(service: MockPaymentMethodsService())..loadMethods(), create: (_) => RecipientsProvider(),
update: (context, organizations, provider) => provider!..updateProviders(organizations),
),
ChangeNotifierProxyProvider2<OrganizationsProvider, RecipientsProvider, PaymentMethodsProvider>(
create: (_) => PaymentMethodsProvider(),
update: (context, organizations, recipients, provider) => provider!..updateProviders(organizations, recipients),
), ),
ChangeNotifierProxyProvider<OrganizationsProvider, WalletsProvider>( ChangeNotifierProxyProvider<OrganizationsProvider, WalletsProvider>(
create: (_) => WalletsProvider(ApiWalletsService()), create: (_) => WalletsProvider(ApiWalletsService()),
@@ -90,18 +93,11 @@ void main() async {
ChangeNotifierProvider( ChangeNotifierProvider(
create: (_) => MockPaymentProvider(), create: (_) => MockPaymentProvider(),
), ),
ChangeNotifierProvider(
create: (_) => RecipientProvider(RecipientService())..loadRecipients(),
),
ChangeNotifierProxyProvider3<RecipientProvider, WalletsProvider, PaymentMethodsProvider, PageSelectorProvider>( ChangeNotifierProxyProvider3<RecipientsProvider, WalletsProvider, PaymentMethodsProvider, PageSelectorProvider>(
create: (context) => PageSelectorProvider(), create: (context) => PageSelectorProvider(),
update: (context, recipientProv, walletsProv, methodsProv, previous) => update: (context, recipientProv, walletsProv, methodsProv, previous) =>
previous ?? PageSelectorProvider( previous ?? PageSelectorProvider()..update(recipientProv, walletsProv, methodsProv),
recipientProvider: recipientProv,
walletsProvider: walletsProv,
methodsProvider: methodsProv,
)..update(recipientProv, walletsProv, methodsProv),
), ),
ChangeNotifierProvider( ChangeNotifierProvider(

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/card.dart'; import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/crypto_address.dart'; import 'package:pshared/models/payment/methods/crypto_address.dart';
import 'package:pshared/models/payment/methods/iban.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/recipient.dart';
import 'package:pshared/models/recipient/status.dart'; import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/models/recipient/type.dart'; import 'package:pshared/models/recipient/type.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pweb/pages/address_book/form/view.dart'; import 'package:pweb/pages/address_book/form/view.dart';
import 'package:pweb/services/amplitude.dart'; import 'package:pweb/services/amplitude.dart';
@@ -38,8 +41,8 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
void initState() { void initState() {
super.initState(); super.initState();
final r = widget.recipient; final r = widget.recipient;
_nameCtrl = TextEditingController(text: r?.name ?? ""); _nameCtrl = TextEditingController(text: r?.name ?? '');
_emailCtrl = TextEditingController(text: r?.email ?? ""); _emailCtrl = TextEditingController(text: r?.email ?? '');
_type = r?.type ?? RecipientType.internal; _type = r?.type ?? RecipientType.internal;
_status = r?.status ?? RecipientStatus.ready; _status = r?.status ?? RecipientStatus.ready;
@@ -50,7 +53,7 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
if (r?.cryptoAddress != null) _methods[PaymentType.cryptoAddress] = r!.cryptoAddress; if (r?.cryptoAddress != null) _methods[PaymentType.cryptoAddress] = r!.cryptoAddress;
} }
//TODO Change when registration is ready //TODO: Change when registration is ready
void _save() { void _save() {
if (!_formKey.currentState!.validate() || _methods.isEmpty) { if (!_formKey.currentState!.validate() || _methods.isEmpty) {
AmplitudeService.recipientAddCompleted( AmplitudeService.recipientAddCompleted(
@@ -66,47 +69,41 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
return; return;
} }
final recipient = Recipient( final recipient = newRecipient(
name: _nameCtrl.text, name: _nameCtrl.text,
email: _emailCtrl.text, email: _emailCtrl.text,
type: _type, type: _type,
status: _status, status: _status,
avatarUrl: null, avatarUrl: null,
card: _methods[PaymentType.card] as CardPaymentMethod?, organizationRef: context.read<OrganizationsProvider>().current.id
iban: _methods[PaymentType.iban] as IbanPaymentMethod?,
wallet: _methods[PaymentType.wallet] as WalletPaymentMethod?,
bank: _methods[PaymentType.bankAccount] as RussianBankAccountPaymentMethod?,
cryptoAddress: _methods[PaymentType.cryptoAddress] as CryptoAddressPaymentMethod?,
); );
widget.onSaved?.call(recipient); widget.onSaved?.call(recipient);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => FormView(
return FormView( formKey: _formKey,
formKey: _formKey, nameCtrl: _nameCtrl,
nameCtrl: _nameCtrl, emailCtrl: _emailCtrl,
emailCtrl: _emailCtrl, type: _type,
type: _type, status: _status,
status: _status, methods: _methods,
methods: _methods, onTypeChanged: (t) => setState(() => _type = t),
onTypeChanged: (t) => setState(() => _type = t), onStatusChanged: (s) => setState(() => _status = s),
onStatusChanged: (s) => setState(() => _status = s), onMethodsChanged: (type, data) {
onMethodsChanged: (type, data) { setState(() {
setState(() { if (data != null) {
if (data != null) { _methods[type] = data;
_methods[type] = data; } else {
} else { _methods.remove(type);
_methods.remove(type); }
} });
}); },
}, onSave: _save,
onSave: _save, isEditing: widget.recipient != null,
isEditing: widget.recipient != null, onBack: () {
onBack: () { widget.onSaved?.call(null);
widget.onSaved?.call(null); },
}, );
);
}
} }

View File

@@ -11,13 +11,11 @@ class PaymentInfoRow extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Row(
return Row( children: [
children: [ Text(label, style: Theme.of(context).textTheme.bodySmall),
Text(label, style: Theme.of(context).textTheme.bodySmall), const SizedBox(width: 8),
const SizedBox(width: 8), Text(value, style: Theme.of(context).textTheme.bodySmall),
Text(value, style: Theme.of(context).textTheme.bodySmall), ],
], );
);
}
} }

View File

@@ -1,36 +1,60 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart'; import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart'; import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart'; import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart';
import 'package:pweb/utils/payment/label.dart'; import 'package:pweb/utils/payment/label.dart';
class RecipientItem extends StatelessWidget { class RecipientItem extends StatefulWidget {
final Recipient recipient;
final VoidCallback onTap;
static const double _horizontalPadding = 16.0; static const double _horizontalPadding = 16.0;
static const double _verticalPadding = 8.0; static const double _verticalPadding = 8.0;
static const double _avatarRadius = 20; static const double _avatarRadius = 20;
static const double _spacingWidth = 12; static const double _spacingWidth = 12;
final Recipient recipient;
final VoidCallback onTap;
const RecipientItem({ const RecipientItem({
super.key, super.key,
required this.recipient, required this.recipient,
required this.onTap, required this.onTap,
}); });
@override
State<RecipientItem> createState() => _RecipientItemState();
}
class _RecipientItemState extends State<RecipientItem> {
late PaymentMethodsProvider _methodsProvider;
@override
void initState() {
super.initState();
_methodsProvider = PaymentMethodsProvider();
_methodsProvider.updateProviders(
context.read<OrganizationsProvider>(),
context.read<RecipientsProvider>(),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator());
final recipient = widget.recipient;
return InkWell( return InkWell(
onTap: onTap, onTap: widget.onTap,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: _horizontalPadding, horizontal: RecipientItem._horizontalPadding,
vertical: _verticalPadding, vertical: RecipientItem._verticalPadding,
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -42,43 +66,20 @@ class RecipientItem extends StatelessWidget {
isVisible: false, isVisible: false,
name: recipient.name, name: recipient.name,
avatarUrl: recipient.avatarUrl, avatarUrl: recipient.avatarUrl,
avatarRadius: _avatarRadius, avatarRadius: RecipientItem._avatarRadius,
nameStyle: Theme.of(context).textTheme.bodyMedium, nameStyle: Theme.of(context).textTheme.bodyMedium,
), ),
title: Text(recipient.name), title: Text(recipient.name),
subtitle: Text(recipient.email), subtitle: Text(recipient.email),
), ),
), ),
const SizedBox(width: _spacingWidth), const SizedBox(width: RecipientItem._spacingWidth),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: _methodsProvider.methods.map((m) => PaymentInfoRow(
if (recipient.bank?.accountNumber.isNotEmpty == true) label: getPaymentTypeLabel(context, m.type),
PaymentInfoRow( value: _displayString(m),
label: getPaymentTypeLabel(context, PaymentType.bankAccount), )).toList(),
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,
),
],
), ),
], ],
), ),

View File

@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/address_book/page/search.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/pages/dashboard/payouts/single/adress_book/short_list.dart';
import 'package:pweb/providers/recipient.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -39,7 +39,7 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final provider = context.read<RecipientProvider>(); final provider = context.read<RecipientsProvider>();
_searchController = TextEditingController(text: provider.query); _searchController = TextEditingController(text: provider.query);
_searchController.addListener(() { _searchController.addListener(() {
@@ -57,7 +57,7 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
final provider = context.watch<RecipientProvider>(); final provider = context.watch<RecipientsProvider>();
if (provider.isLoading) { if (provider.isLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@@ -88,14 +88,14 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
const SizedBox(height: _spacingBetween), const SizedBox(height: _spacingBetween),
Expanded( Expanded(
child: _isExpanded child: _isExpanded
? LongListAdressBookPayout( ? LongListAdressBookPayout(
filteredRecipients: provider.filteredRecipients, filteredRecipients: provider.filteredRecipients,
onSelected: widget.onSelected, onSelected: widget.onSelected,
) )
: ShortListAdressBookPayout( : ShortListAdressBookPayout(
recipients: provider.recipients, recipients: provider.recipients,
onSelected: widget.onSelected, onSelected: widget.onSelected,
), ),
), ),
], ],
), ),

View File

@@ -1,27 +1,25 @@
import 'package:flutter/material.dart'; 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'; import 'package:pweb/utils/payment/dropdown.dart';
class PaymentMethodSelector extends StatelessWidget { class PaymentMethodSelector extends StatelessWidget {
final PaymentMethodsProvider methodsProvider;
final ValueChanged<PaymentMethod> onMethodChanged; final ValueChanged<PaymentMethod> onMethodChanged;
const PaymentMethodSelector({ const PaymentMethodSelector({
super.key, super.key,
required this.methodsProvider,
required this.onMethodChanged, required this.onMethodChanged,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Consumer<PaymentMethodsProvider>(builder:(context, provider, _) => PaymentMethodDropdown(
return PaymentMethodDropdown( methods: provider.methods,
methods: methodsProvider.methods, initialValue: provider.currentObject,
initialValue: methodsProvider.selectedMethod, onChanged: onMethodChanged,
onChanged: onMethodChanged, ));
);
}
} }

View File

@@ -3,12 +3,12 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/providers/payment_flow_provider.dart'; import 'package:pweb/providers/payment_flow_provider.dart';
import 'package:pweb/pages/payment_methods/widgets/payment_page_body.dart'; import 'package:pweb/pages/payment_methods/widgets/payment_page_body.dart';
import 'package:pweb/providers/page_selector.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 { class PaymentPage extends StatefulWidget {
@@ -49,30 +49,22 @@ class _PaymentPageState extends State<PaymentPage> {
void _initializePaymentPage() { void _initializePaymentPage() {
final pageSelector = context.read<PageSelectorProvider>(); final pageSelector = context.read<PageSelectorProvider>();
final methodsProvider = context.read<PaymentMethodsProvider>(); final methodsProvider = context.read<PaymentMethodsProvider>();
final recipientProvider = context.read<RecipientProvider>(); final recipientProvider = context.read<RecipientsProvider>();
pageSelector.handleWalletAutoSelection(); pageSelector.handleWalletAutoSelection();
if (methodsProvider.methods.isEmpty && !methodsProvider.isLoading) {
methodsProvider.loadMethods();
}
if (recipientProvider.recipients.isEmpty && !recipientProvider.isLoading) {
recipientProvider.loadRecipients();
}
_flowProvider.syncWithSelector(pageSelector); _flowProvider.syncWithSelector(pageSelector);
} }
void _handleSearchChanged(String query) { void _handleSearchChanged(String query) {
context.read<RecipientProvider>().setQuery(query); context.read<RecipientsProvider>().setQuery(query);
} }
void _handleRecipientSelected(Recipient recipient) { void _handleRecipientSelected(Recipient recipient) {
final pageSelector = context.read<PageSelectorProvider>(); final pageSelector = context.read<PageSelectorProvider>();
final recipientProvider = context.read<RecipientProvider>(); final recipientProvider = context.read<RecipientsProvider>();
recipientProvider.selectRecipient(recipient); recipientProvider.setCurrentObject(recipient.id);
pageSelector.selectRecipient(recipient); pageSelector.selectRecipient(recipient);
_flowProvider.reset(pageSelector); _flowProvider.reset(pageSelector);
_clearSearchField(); _clearSearchField();
@@ -80,9 +72,9 @@ class _PaymentPageState extends State<PaymentPage> {
void _handleRecipientCleared() { void _handleRecipientCleared() {
final pageSelector = context.read<PageSelectorProvider>(); final pageSelector = context.read<PageSelectorProvider>();
final recipientProvider = context.read<RecipientProvider>(); final recipientProvider = context.read<RecipientsProvider>();
recipientProvider.selectRecipient(null); recipientProvider.setCurrentObject(null);
pageSelector.selectRecipient(null); pageSelector.selectRecipient(null);
_flowProvider.reset(pageSelector); _flowProvider.reset(pageSelector);
_clearSearchField(); _clearSearchField();
@@ -91,7 +83,7 @@ class _PaymentPageState extends State<PaymentPage> {
void _clearSearchField() { void _clearSearchField() {
_searchController.clear(); _searchController.clear();
_searchFocusNode.unfocus(); _searchFocusNode.unfocus();
context.read<RecipientProvider>().setQuery(''); context.read<RecipientsProvider>().setQuery('');
} }
void _handleSendPayment() { void _handleSendPayment() {

View File

@@ -31,7 +31,7 @@ class PaymentMethodTile extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
return Opacity( return Opacity(
opacity: method.isEnabled ? 1 : 0.5, opacity: method.isArchived ? 1 : 0.5,
child: Card( child: Card(
margin: const EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(vertical: 4),
elevation: 0, elevation: 0,
@@ -41,11 +41,12 @@ class PaymentMethodTile extends StatelessWidget {
onTap: makeMain, onTap: makeMain,
title: Row( title: Row(
children: [ children: [
Expanded(child: Text(method.label)), Expanded(child: Text(method.name)),
Text( if (method.description != null)
method.details, Text(
style: theme.textTheme.bodySmall, method.description!,
), style: theme.textTheme.bodySmall,
),
], ],
), ),
trailing: Row( trailing: Row(
@@ -73,12 +74,10 @@ class PaymentMethodTile extends StatelessWidget {
); );
} }
Widget _buildEnabledSwitch() { Widget _buildEnabledSwitch() => Switch.adaptive(
return Switch.adaptive( value: method.isArchived,
value: method.isEnabled, onChanged: toggleEnabled,
onChanged: toggleEnabled, );
);
}
Widget _buildPopupMenu(AppLocalizations l10n) { Widget _buildPopupMenu(AppLocalizations l10n) {
return PopupMenuButton<String>( return PopupMenuButton<String>(

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payment_methods/header.dart'; import 'package:pweb/pages/payment_methods/header.dart';
import 'package:pweb/pages/payment_methods/method_selector.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/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/providers/page_selector.dart';
import 'package:pweb/providers/payment_flow_provider.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/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -43,7 +45,7 @@ class PaymentPageBody extends StatelessWidget {
final dimensions = AppDimensions(); final dimensions = AppDimensions();
final pageSelector = context.watch<PageSelectorProvider>(); final pageSelector = context.watch<PageSelectorProvider>();
final methodsProvider = context.watch<PaymentMethodsProvider>(); final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipientProvider = context.watch<RecipientProvider>(); final recipientProvider = context.watch<RecipientsProvider>();
final flowProvider = context.watch<PaymentFlowProvider>(); final flowProvider = context.watch<PaymentFlowProvider>();
final recipient = pageSelector.selectedRecipient; final recipient = pageSelector.selectedRecipient;
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
@@ -79,8 +81,7 @@ class PaymentPageBody extends StatelessWidget {
SectionTitle(loc.sourceOfFunds), SectionTitle(loc.sourceOfFunds),
SizedBox(height: dimensions.paddingSmall), SizedBox(height: dimensions.paddingSmall),
PaymentMethodSelector( PaymentMethodSelector(
methodsProvider: methodsProvider, onMethodChanged: (m) => methodsProvider.setCurrentObject(m.id),
onMethodChanged: methodsProvider.selectMethod,
), ),
SizedBox(height: dimensions.paddingXLarge), SizedBox(height: dimensions.paddingXLarge),

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/address_book/page/search.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/card.dart';
import 'package:pweb/pages/payment_methods/widgets/search.dart'; import 'package:pweb/pages/payment_methods/widgets/search.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.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/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.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 { class RecipientSection extends StatelessWidget {
final Recipient? recipient; final Recipient? recipient;
final AppDimensions dimensions; final AppDimensions dimensions;
final RecipientProvider recipientProvider; final RecipientsProvider recipientProvider;
final TextEditingController searchController; final TextEditingController searchController;
final FocusNode searchFocusNode; final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged; final ValueChanged<String> onSearchChanged;

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/providers/recipient.dart';
import 'package:pweb/utils/dimensions.dart'; import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.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 { class RecipientSearchResults extends StatelessWidget {
final AppDimensions dimensions; final AppDimensions dimensions;
final RecipientProvider recipientProvider; final RecipientsProvider recipientProvider;
final ValueChanged<Recipient> onRecipientSelected; final ValueChanged<Recipient> onRecipientSelected;
const RecipientSearchResults({ const RecipientSearchResults({

View File

@@ -4,8 +4,8 @@ import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/type.dart'; import 'package:pshared/models/payment/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/pages/payment_methods/add/widget.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -16,18 +16,10 @@ class PaymentConfigController {
PaymentConfigController(this.context); PaymentConfigController(this.context);
void loadMethods() { Future<void> addMethod() async => showDialog<PaymentMethodData>(
context.read<PaymentMethodsProvider>().loadMethods(); context: context,
} builder: (_) => const AddPaymentMethodDialog(),
);
Future<void> addMethod() async {
final methodsProvider = context.read<PaymentMethodsProvider>();
await showDialog<PaymentMethodData>(
context: context,
builder: (_) => const AddPaymentMethodDialog(),
);
methodsProvider.loadMethods();
}
Future<void> editMethod(PaymentMethod method) async { Future<void> editMethod(PaymentMethod method) async {
// TODO: implement edit functionality // TODO: implement edit functionality
@@ -55,12 +47,12 @@ class PaymentConfigController {
); );
if (confirmed == true) { if (confirmed == true) {
methodsProvider.deleteMethod(method); methodsProvider.delete(method.id);
} }
} }
void toggleEnabled(PaymentMethod method, bool value) { void toggleEnabled(PaymentMethod method, bool value) {
context.read<PaymentMethodsProvider>().toggleEnabled(method, value); context.read<PaymentMethodsProvider>().setArchivedMethod(method: method, newIsArchived: value);
} }
void makeMain(PaymentMethod method) { void makeMain(PaymentMethod method) {
@@ -68,6 +60,7 @@ class PaymentConfigController {
} }
void reorder(int oldIndex, int newIndex) { void reorder(int oldIndex, int newIndex) {
context.read<PaymentMethodsProvider>().reorderMethods(oldIndex, newIndex); // TODO: rimplement on top of Indexable
// context.read<PaymentMethodsProvider>().reorderMethods(oldIndex, newIndex);
} }
} }

View File

@@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.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/payment_methods/title.dart';
import 'package:pweb/pages/payout_page/methods/controller.dart'; import 'package:pweb/pages/payout_page/methods/controller.dart';
import 'package:pweb/providers/payment_methods.dart';
class PaymentConfigList extends StatelessWidget { class PaymentConfigList extends StatelessWidget {

View File

@@ -20,7 +20,6 @@ class _MethodsWidgetState extends State<MethodsWidget> {
void initState() { void initState() {
super.initState(); super.initState();
controller = PaymentConfigController(context); controller = PaymentConfigController(context);
controller.loadMethods();
} }
@override @override

View File

@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.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/methods/widget.dart';
import 'package:pweb/pages/payout_page/wallet/wigets.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'; import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -1,41 +1,40 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.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/methods/type.dart';
import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/models/wallet.dart'; import 'package:pweb/models/wallet.dart';
import 'package:pweb/providers/payment_methods.dart';
import 'package:pweb/providers/wallets.dart'; import 'package:pweb/providers/wallets.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/services/amplitude.dart'; import 'package:pweb/services/amplitude.dart';
import 'package:pweb/providers/recipient.dart'; import 'package:pweb/widgets/sidebar/destinations.dart';
class PageSelectorProvider extends ChangeNotifier { class PageSelectorProvider extends ChangeNotifier {
static final _logger = Logger('provider.page_selector');
PayoutDestination _selected = PayoutDestination.dashboard; PayoutDestination _selected = PayoutDestination.dashboard;
PaymentType? _type; PaymentType? _type;
bool _cameFromRecipientList = false; bool _cameFromRecipientList = false;
PayoutDestination? _previousDestination; PayoutDestination? _previousDestination;
RecipientProvider? recipientProvider; late RecipientsProvider recipientProvider;
WalletsProvider? walletsProvider; late WalletsProvider walletsProvider;
PaymentMethodsProvider? methodsProvider; late PaymentMethodsProvider methodsProvider;
PayoutDestination get selected => _selected; PayoutDestination get selected => _selected;
PaymentType? get type => _type; PaymentType? get type => _type;
bool get cameFromRecipientList => _cameFromRecipientList; bool get cameFromRecipientList => _cameFromRecipientList;
PageSelectorProvider({ PageSelectorProvider();
this.recipientProvider,
this.walletsProvider,
this.methodsProvider,
});
void update( void update(
RecipientProvider recipientProv, RecipientsProvider recipientProv,
WalletsProvider walletsProv, WalletsProvider walletsProv,
PaymentMethodsProvider methodsProv, PaymentMethodsProvider methodsProv,
) { ) {
@@ -50,44 +49,30 @@ class PageSelectorProvider extends ChangeNotifier {
} }
void selectRecipient(Recipient? recipient, {bool fromList = false}) { void selectRecipient(Recipient? recipient, {bool fromList = false}) {
if (recipientProvider != null) { recipientProvider.setCurrentObject(recipient?.id);
recipientProvider!.selectRecipient(recipient); _cameFromRecipientList = fromList;
_cameFromRecipientList = fromList; _setPreviousDestination();
_setPreviousDestination(); _selected = PayoutDestination.payment;
_selected = PayoutDestination.payment; notifyListeners();
notifyListeners();
} else {
debugPrint("RecipientProvider is null — cannot select recipient");
}
} }
void editRecipient(Recipient? recipient, {bool fromList = false}) { void editRecipient(Recipient? recipient, {bool fromList = false}) {
if (recipientProvider != null) { recipientProvider.setCurrentObject(recipient?.id);
recipientProvider!.selectRecipient(recipient); _cameFromRecipientList = fromList;
_cameFromRecipientList = fromList; _selected = PayoutDestination.addrecipient;
_selected = PayoutDestination.addrecipient; notifyListeners();
notifyListeners();
} else {
debugPrint("RecipientProvider is null — cannot select recipient");
}
} }
void goToAddRecipient() { void goToAddRecipient() {
if (recipientProvider != null) { AmplitudeService.recipientAddStarted();
AmplitudeService.recipientAddStarted(); recipientProvider!.setCurrentObject(null);
recipientProvider!.selectRecipient(null); _selected = PayoutDestination.addrecipient;
_selected = PayoutDestination.addrecipient; _cameFromRecipientList = false;
_cameFromRecipientList = false; notifyListeners();
notifyListeners();
} else {
debugPrint("RecipientProvider is null — cannot go to add recipient");
}
} }
void startPaymentWithoutRecipient(PaymentType type) { void startPaymentWithoutRecipient(PaymentType type) {
if (recipientProvider != null) { recipientProvider.setCurrentObject(null);
recipientProvider!.selectRecipient(null);
}
_type = type; _type = type;
_cameFromRecipientList = false; _cameFromRecipientList = false;
_setPreviousDestination(); _setPreviousDestination();
@@ -111,13 +96,9 @@ class PageSelectorProvider extends ChangeNotifier {
} }
void selectWallet(Wallet wallet) { void selectWallet(Wallet wallet) {
if (walletsProvider != null) { walletsProvider.selectWallet(wallet);
walletsProvider!.selectWallet(wallet); _selected = PayoutDestination.editwallet;
_selected = PayoutDestination.editwallet; notifyListeners();
notifyListeners();
} else {
debugPrint("WalletsProvider is null — cannot select wallet");
}
} }
void startPaymentFromWallet(Wallet wallet) { void startPaymentFromWallet(Wallet wallet) {
@@ -129,26 +110,26 @@ class PageSelectorProvider extends ChangeNotifier {
} }
PaymentMethod? getPaymentMethodForWallet(Wallet wallet) { PaymentMethod? getPaymentMethodForWallet(Wallet wallet) {
if (methodsProvider == null || methodsProvider!.methods.isEmpty) { if (methodsProvider.methods.isEmpty) {
return null; return null;
} }
return methodsProvider!.methods.firstWhereOrNull( return methodsProvider!.methods.firstWhereOrNull(
(method) => method.type == PaymentType.wallet && (method) => method.type == PaymentType.wallet &&
method.details.contains(wallet.walletUserID) (method.description?.contains(wallet.walletUserID) ?? false),
); );
} }
Map<PaymentType, Object> getAvailablePaymentTypes() { Map<PaymentType, Object> getAvailablePaymentTypes() {
final recipient = selectedRecipient; 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 { return {
if (recipient.card != null) PaymentType.card: recipient.card!, for (final method in methodsForRecipient) method.type: method.data,
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!,
}; };
} }
@@ -158,11 +139,11 @@ class PageSelectorProvider extends ChangeNotifier {
if (availableTypes.containsKey(currentType)) { if (availableTypes.containsKey(currentType)) {
return 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() { bool shouldShowPaymentForm() {
@@ -170,11 +151,11 @@ class PageSelectorProvider extends ChangeNotifier {
} }
void handleWalletAutoSelection() { void handleWalletAutoSelection() {
if (selectedWallet != null && methodsProvider != null) { if (selectedWallet != null) {
final wallet = selectedWallet!; final wallet = selectedWallet!;
final matchingMethod = getPaymentMethodForWallet(wallet); final matchingMethod = getPaymentMethodForWallet(wallet);
if (matchingMethod != null) { if (matchingMethod != null) {
methodsProvider!.selectMethod(matchingMethod); methodsProvider.setCurrentObject(matchingMethod.id);
} }
} }
} }
@@ -185,6 +166,6 @@ class PageSelectorProvider extends ChangeNotifier {
} }
} }
Recipient? get selectedRecipient => recipientProvider?.selectedRecipient; Recipient? get selectedRecipient => recipientProvider.currentObject;
Wallet? get selectedWallet => walletsProvider?.selectedWallet; Wallet? get selectedWallet => walletsProvider.selectedWallet;
} }

View File

@@ -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<PaymentMethod> _methods = [];
PaymentMethod? _selectedMethod;
bool _isLoading = false;
String? _error;
PaymentMethodsProvider({required this.service});
List<PaymentMethod> get methods => _methods;
PaymentMethod? get selectedMethod => _selectedMethod;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> 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);
}
}

View File

@@ -1,10 +1,9 @@
import 'package:pshared/models/payment/upload_history_item.dart'; import 'package:pshared/models/payment/upload_history_item.dart';
import 'package:pweb/providers/template.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<List<UploadHistoryItem>> { class UploadHistoryProvider extends FutureProviderTemplate<List<UploadHistoryItem>> {
UploadHistoryProvider({required UploadHistoryService service}) UploadHistoryProvider({required UploadHistoryService service}) : super(loader: service.fetchHistory);
: super(loader: service.fetchHistory);
} }

View File

@@ -0,0 +1,48 @@
// import 'package:pshared/models/payment/methods/type.dart';
// import 'package:pshared/models/payment/type.dart';
// abstract class PaymentMethodsService {
// Future<List<PaymentMethod>> fetchMethods();
// }
// class MockPaymentMethodsService implements PaymentMethodsService {
// @override
// Future<List<PaymentMethod>> 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: 'WA12345667',
// 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,
// ),
// ];
// }
// }

View File

@@ -1,48 +0,0 @@
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/payment/type.dart';
abstract class PaymentMethodsService {
Future<List<PaymentMethod>> fetchMethods();
}
class MockPaymentMethodsService implements PaymentMethodsService {
@override
Future<List<PaymentMethod>> 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: 'WA12345667',
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,
),
];
}
}

View File

@@ -1,88 +1,88 @@
import 'package:pshared/models/recipient/recipient.dart'; // import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/payment/methods/card.dart'; // import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/iban.dart'; // import 'package:pshared/models/payment/methods/iban.dart';
import 'package:pshared/models/payment/methods/russian_bank.dart'; // import 'package:pshared/models/payment/methods/russian_bank.dart';
import 'package:pshared/models/payment/methods/wallet.dart'; // import 'package:pshared/models/payment/methods/wallet.dart';
import 'package:pshared/models/recipient/status.dart'; // import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/models/recipient/type.dart'; // import 'package:pshared/models/recipient/type.dart';
class RecipientService { // class RecipientService {
Future<List<Recipient>> fetchRecipients() async { // Future<List<Recipient>> fetchRecipients() async {
await Future.delayed(const Duration(milliseconds: 500)); // await Future.delayed(const Duration(milliseconds: 500));
return RecipientMockData.all; // return RecipientMockData.all;
} // }
} // }
class RecipientMockData { // class RecipientMockData {
static List<Recipient> get all => [ // static List<Recipient> get all => [
Recipient.mock( // Recipient.mock(
name: 'Alice Johnson', // name: 'Alice Johnson',
email: 'alice@example.com', // email: 'alice@example.com',
status: RecipientStatus.ready, // status: RecipientStatus.ready,
type: RecipientType.internal, // type: RecipientType.internal,
card: CardPaymentMethod( // card: CardPaymentMethod(
pan: '1213', // pan: '1213',
firstName: 'Alice', // firstName: 'Alice',
lastName: 'Johnson', // lastName: 'Johnson',
), // ),
), // ),
Recipient.mock( // Recipient.mock(
name: 'Bob & Co Ltd.', // name: 'Bob & Co Ltd.',
email: 'payout@bobco.com', // email: 'payout@bobco.com',
status: RecipientStatus.registered, // status: RecipientStatus.registered,
type: RecipientType.external, // type: RecipientType.external,
card: CardPaymentMethod( // card: CardPaymentMethod(
pan: '4343', // pan: '4343',
firstName: 'Bob', // firstName: 'Bob',
lastName: 'Co', // lastName: 'Co',
), // ),
iban: IbanPaymentMethod( // iban: IbanPaymentMethod(
iban: 'FR7630***890189', // iban: 'FR7630***890189',
accountHolder: 'Bob & Co Ltd.', // accountHolder: 'Bob & Co Ltd.',
bic: 'AGRIFRPP', // bic: 'AGRIFRPP',
bankName: 'Credit Agricole', // bankName: 'Credit Agricole',
), // ),
wallet: WalletPaymentMethod(walletId: '8932231'), // wallet: WalletPaymentMethod(walletId: '8932231'),
), // ),
Recipient.mock( // Recipient.mock(
name: 'Carlos Kline', // name: 'Carlos Kline',
email: 'carlos@acme.org', // email: 'carlos@acme.org',
status: RecipientStatus.notRegistered, // status: RecipientStatus.notRegistered,
type: RecipientType.internal, // type: RecipientType.internal,
wallet: WalletPaymentMethod(walletId: '7723490'), // wallet: WalletPaymentMethod(walletId: '7723490'),
), // ),
Recipient.mock( // Recipient.mock(
name: 'Delta Outsourcing GmbH', // name: 'Delta Outsourcing GmbH',
email: 'finance@delta-os.de', // email: 'finance@delta-os.de',
status: RecipientStatus.registered, // status: RecipientStatus.registered,
type: RecipientType.external, // type: RecipientType.external,
card: CardPaymentMethod( // card: CardPaymentMethod(
pan: '9988', // pan: '9988',
firstName: 'Delta', // firstName: 'Delta',
lastName: 'GmbH', // lastName: 'GmbH',
), // ),
iban: IbanPaymentMethod( // iban: IbanPaymentMethod(
iban: 'DE4450***324931', // iban: 'DE4450***324931',
accountHolder: 'Delta Outsourcing GmbH', // accountHolder: 'Delta Outsourcing GmbH',
bic: 'INGDDEFFXXX', // bic: 'INGDDEFFXXX',
bankName: 'ING', // bankName: 'ING',
), // ),
), // ),
Recipient.mock( // Recipient.mock(
name: 'Erin Patel', // name: 'Erin Patel',
email: 'erin@labster.io', // email: 'erin@labster.io',
status: RecipientStatus.ready, // status: RecipientStatus.ready,
type: RecipientType.internal, // type: RecipientType.internal,
bank: RussianBankAccountPaymentMethod( // bank: RussianBankAccountPaymentMethod(
accountNumber: '4081***7654', // accountNumber: '4081***7654',
recipientName: 'Erin Patel', // recipientName: 'Erin Patel',
inn: '7812012345', // inn: '7812012345',
kpp: '781201001', // kpp: '781201001',
bankName: 'Alfa-Bank', // bankName: 'Alfa-Bank',
bik: '044525593', // bik: '044525593',
correspondentAccount: '30101810200000000593', // correspondentAccount: '30101810200000000593',
), // ),
), // ),
]; // ];
} // }

View File

@@ -48,7 +48,7 @@ class _PaymentMethodDropdownState extends State<PaymentMethodDropdown> {
children: [ children: [
Icon(iconForPaymentType(method.type), size: 20), Icon(iconForPaymentType(method.type), size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text('${method.label} (${method.details})'), Text('${method.name}' + (method.description == null ? '' : ' (${method.description!})')),
], ],
), ),
); );

View File

@@ -12,6 +12,6 @@ String getPaymentTypeLabel(BuildContext context, PaymentType type) {
PaymentType.bankAccount => l10n.paymentTypeBankAccount, PaymentType.bankAccount => l10n.paymentTypeBankAccount,
PaymentType.iban => l10n.paymentTypeIban, PaymentType.iban => l10n.paymentTypeIban,
PaymentType.wallet => l10n.paymentTypeWallet, PaymentType.wallet => l10n.paymentTypeWallet,
PaymentType.cryptoAddress => 'Crypto address', PaymentType.cryptoAddress => l10n.paymentTypeCryptoAddress,
}; };
} }

View File

@@ -72,7 +72,7 @@ class PageSelector extends StatelessWidget {
break; break;
case PayoutDestination.addrecipient: case PayoutDestination.addrecipient:
final recipient = provider.recipientProvider?.selectedRecipient; final recipient = provider.recipientProvider.currentObject;
content = AdressBookRecipientForm( content = AdressBookRecipientForm(
recipient: recipient, recipient: recipient,
onSaved: (_) => provider.selectPage(PayoutDestination.recipients), onSaved: (_) => provider.selectPage(PayoutDestination.recipients),
@@ -100,7 +100,7 @@ class PageSelector extends StatelessWidget {
break; break;
case PayoutDestination.editwallet: case PayoutDestination.editwallet:
final wallet = provider.walletsProvider?.selectedWallet; final wallet = provider.walletsProvider.selectedWallet;
content = wallet != null content = wallet != null
? WalletEditPage( ? WalletEditPage(
onBack: provider.goBackFromWalletEdit, onBack: provider.goBackFromWalletEdit,