Compare commits

4 Commits

Author SHA1 Message Date
b481de9ffc Merge pull request 'New comments section in the requests/responses' (#679) from bff-677 into main
Reviewed-on: #679
2026-03-05 19:29:10 +00:00
Stephan D
0c29e7686d New comments section in the requests/responses 2026-03-05 20:28:28 +01:00
5b26a70a15 Merge pull request 'New comments section in the requests/responses' (#678) from bff-677 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #678
2026-03-05 19:28:05 +00:00
Stephan D
b832c2a7c4 New comments section in the requests/responses 2026-03-05 20:27:45 +01:00
75 changed files with 854 additions and 1307 deletions

View File

@@ -1,10 +1,31 @@
# Sendico Development Environment - Makefile # Sendico Development Environment - Makefile
# Docker Compose + Makefile build system # Docker Compose + Makefile build system
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend .PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend backend-up backend-down backend-rebuild
COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev
SERVICE ?= SERVICE ?=
BACKEND_SERVICES := \
dev-discovery \
dev-fx-oracle \
dev-fx-ingestor \
dev-billing-fees \
dev-billing-documents \
dev-ledger \
dev-payments-orchestrator \
dev-payments-quotation \
dev-payments-methods \
dev-chain-gateway-vault-agent \
dev-chain-gateway \
dev-tron-gateway-vault-agent \
dev-tron-gateway \
dev-aurora-gateway \
dev-tgsettle-gateway \
dev-notification \
dev-callbacks-vault-agent \
dev-callbacks \
dev-bff-vault-agent \
dev-bff
# Colors # Colors
GREEN := \033[0;32m GREEN := \033[0;32m
@@ -31,6 +52,9 @@ help:
@echo "$(YELLOW)Selective Operations:$(NC)" @echo "$(YELLOW)Selective Operations:$(NC)"
@echo " make infra-up Start infrastructure only (mongo, nats, vault)" @echo " make infra-up Start infrastructure only (mongo, nats, vault)"
@echo " make services-up Start application services only" @echo " make services-up Start application services only"
@echo " make backend-up Start backend services only (no infrastructure/frontend)"
@echo " make backend-down Stop backend services only"
@echo " make backend-rebuild Rebuild and restart backend services only"
@echo " make list-services List all available services" @echo " make list-services List all available services"
@echo "" @echo ""
@echo "$(YELLOW)Build Groups:$(NC)" @echo "$(YELLOW)Build Groups:$(NC)"
@@ -229,6 +253,21 @@ services-up:
dev-bff \ dev-bff \
dev-frontend dev-frontend
# Backend services only (no infrastructure, no frontend)
backend-up:
@echo "$(GREEN)Starting backend services only (no infra changes)...$(NC)"
@$(COMPOSE) up -d --no-deps $(BACKEND_SERVICES)
backend-down:
@echo "$(YELLOW)Stopping backend services only...$(NC)"
@$(COMPOSE) stop $(BACKEND_SERVICES)
backend-rebuild:
@echo "$(GREEN)Rebuilding backend services only (no infra changes)...$(NC)"
@$(COMPOSE) build $(BACKEND_SERVICES)
@$(COMPOSE) up -d --no-deps --force-recreate $(BACKEND_SERVICES)
@echo "$(GREEN)✅ Backend services rebuilt$(NC)"
# Status check # Status check
status: status:
@$(COMPOSE) ps @$(COMPOSE) ps

View File

@@ -24,6 +24,7 @@ Financial services platform providing payment orchestration, ledger accounting,
| FX Ingestor | `api/fx/ingestor/` | FX rate ingestion | | FX Ingestor | `api/fx/ingestor/` | FX rate ingestion |
| Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway | | Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway |
| Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway | | Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway |
| Gateway Aurora | `api/gateway/aurora/` | Card payouts simulator |
| Gateway MNTX | `api/gateway/mntx/` | Card payouts | | Gateway MNTX | `api/gateway/mntx/` | Card payouts |
| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX | | Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX |
| Notification | `api/notification/` | Notifications | | Notification | `api/notification/` | Notifications |
@@ -31,6 +32,16 @@ Financial services platform providing payment orchestration, ledger accounting,
| Callbacks | `api/edge/callbacks/` | Webhook callbacks delivery | | Callbacks | `api/edge/callbacks/` | Webhook callbacks delivery |
| Frontend | `frontend/pweb/` | Flutter web UI | | Frontend | `frontend/pweb/` | Flutter web UI |
Gateway note: current dev compose workflows (`make services-up`, `make build-gateways`) use Aurora for card-payout flows (`chain`, `tron`, `aurora`, `tgsettle`). The MNTX gateway codebase is retained separately for Monetix-specific integration.
## Prerequisites
- Docker with Docker Compose plugin
- GNU Make
- Go toolchain
- Dart SDK
- Flutter SDK
## Development ## Development
Development uses Docker Compose via the Makefile. Run `make help` for all available commands. Development uses Docker Compose via the Makefile. Run `make help` for all available commands.
@@ -54,6 +65,8 @@ make status # Show service status
make logs # View all logs make logs # View all logs
make logs SERVICE=dev-ledger # View logs for a specific service make logs SERVICE=dev-ledger # View logs for a specific service
make rebuild SERVICE=dev-ledger # Rebuild and restart a specific service make rebuild SERVICE=dev-ledger # Rebuild and restart a specific service
make list-services # List all services and ports
make health # Check service health
make clean # Remove all containers and volumes make clean # Remove all containers and volumes
``` ```
@@ -62,6 +75,10 @@ make clean # Remove all containers and volumes
```bash ```bash
make infra-up # Start infrastructure only (MongoDB, NATS, Vault) make infra-up # Start infrastructure only (MongoDB, NATS, Vault)
make services-up # Start application services only (assumes infra is running) make services-up # Start application services only (assumes infra is running)
make backend-up # Start backend services only (no infrastructure/frontend changes)
make backend-down # Stop backend services only
make backend-rebuild # Rebuild and restart backend services only
make list-services # Show service names, ports, and descriptions
``` ```
### Build Groups ### Build Groups
@@ -69,8 +86,8 @@ make services-up # Start application services only (assumes infra is running)
```bash ```bash
make build-core # discovery, ledger, fees, documents make build-core # discovery, ledger, fees, documents
make build-fx # oracle, ingestor make build-fx # oracle, ingestor
make build-payments # orchestrator make build-payments # orchestrator, quotation, methods
make build-gateways # chain, tron, mntx, tgsettle make build-gateways # chain, tron, aurora, tgsettle
make build-api # notification, callbacks, bff make build-api # notification, callbacks, bff
make build-frontend # Flutter web UI make build-frontend # Flutter web UI
``` ```

View File

@@ -14,6 +14,7 @@ type PaymentIntent struct {
SettlementMode SettlementMode `json:"settlement_mode,omitempty"` SettlementMode SettlementMode `json:"settlement_mode,omitempty"`
FeeTreatment FeeTreatment `json:"fee_treatment,omitempty"` FeeTreatment FeeTreatment `json:"fee_treatment,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"` Attributes map[string]string `json:"attributes,omitempty"`
Comment string `json:"comment,omitempty"`
Customer *Customer `json:"customer,omitempty"` Customer *Customer `json:"customer,omitempty"`
} }

View File

@@ -70,6 +70,7 @@ type Payment struct {
PaymentRef string `json:"paymentRef,omitempty"` PaymentRef string `json:"paymentRef,omitempty"`
IdempotencyKey string `json:"idempotencyKey,omitempty"` IdempotencyKey string `json:"idempotencyKey,omitempty"`
State string `json:"state,omitempty"` State string `json:"state,omitempty"`
Comment string `json:"comment,omitempty"`
FailureCode string `json:"failureCode,omitempty"` FailureCode string `json:"failureCode,omitempty"`
FailureReason string `json:"failureReason,omitempty"` FailureReason string `json:"failureReason,omitempty"`
Operations []PaymentOperation `json:"operations,omitempty"` Operations []PaymentOperation `json:"operations,omitempty"`
@@ -294,6 +295,7 @@ func toPayment(p *orchestrationv2.Payment) *Payment {
return &Payment{ return &Payment{
PaymentRef: p.GetPaymentRef(), PaymentRef: p.GetPaymentRef(),
State: enumJSONName(p.GetState().String()), State: enumJSONName(p.GetState().String()),
Comment: strings.TrimSpace(p.GetIntentSnapshot().GetComment()),
FailureCode: failureCode, FailureCode: failureCode,
FailureReason: failureReason, FailureReason: failureReason,
Operations: operations, Operations: operations,

View File

@@ -121,6 +121,22 @@ func TestToPaymentIgnoresHiddenFailures(t *testing.T) {
} }
} }
func TestToPaymentMapsIntentComment(t *testing.T) {
dto := toPayment(&orchestrationv2.Payment{
PaymentRef: "pay-3",
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
IntentSnapshot: &quotationv2.QuoteIntent{
Comment: " invoice-7 ",
},
})
if dto == nil {
t.Fatal("expected non-nil payment dto")
}
if got, want := dto.Comment, "invoice-7"; got != want {
t.Fatalf("comment mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentQuote_MapsIntentRef(t *testing.T) { func TestToPaymentQuote_MapsIntentRef(t *testing.T) {
dto := toPaymentQuote(&quotationv2.PaymentQuote{ dto := toPaymentQuote(&quotationv2.PaymentQuote{
QuoteRef: "quote-1", QuoteRef: "quote-1",

View File

@@ -61,9 +61,7 @@ func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, e
FeeTreatment: resolvedFeeTreatment, FeeTreatment: resolvedFeeTreatment,
SettlementCurrency: settlementCurrency, SettlementCurrency: settlementCurrency,
Fx: mapFXIntent(intent), Fx: mapFXIntent(intent),
} Comment: strings.TrimSpace(intent.Comment),
if comment := strings.TrimSpace(intent.Attributes["comment"]); comment != "" {
quoteIntent.Comment = comment
} }
return quoteIntent, nil return quoteIntent, nil
} }

View File

@@ -9,7 +9,6 @@ replace github.com/tech/sendico/gateway/common => ../common
require ( require (
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.5
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/tech/sendico/gateway/common v0.1.0 github.com/tech/sendico/gateway/common v0.1.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
@@ -36,6 +35,7 @@ require (
github.com/nats-io/nats.go v1.49.0 // indirect github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/procfs v0.20.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect

View File

@@ -0,0 +1,87 @@
package store
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/gateway/tgsettle/storage"
"github.com/tech/sendico/gateway/tgsettle/storage/model"
"github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.uber.org/zap"
)
const (
treasuryTelegramUsersCollection = "treasury_telegram_users"
fieldTreasuryTelegramUserID = "telegramUserId"
)
type TreasuryTelegramUsers struct {
logger mlogger.Logger
repo repository.Repository
}
func NewTreasuryTelegramUsers(logger mlogger.Logger, db *mongo.Database) (*TreasuryTelegramUsers, error) {
if db == nil {
return nil, merrors.InvalidArgument("mongo database is nil")
}
if logger == nil {
logger = zap.NewNop()
}
logger = logger.Named("treasury_telegram_users").With(zap.String("collection", treasuryTelegramUsersCollection))
repo := repository.CreateMongoRepository(db, treasuryTelegramUsersCollection)
if err := repo.CreateIndex(&ri.Definition{
Keys: []ri.Key{{Field: fieldTreasuryTelegramUserID, Sort: ri.Asc}},
Unique: true,
}); err != nil {
logger.Error("Failed to create treasury telegram users user_id index", zap.Error(err), zap.String("index_field", fieldTreasuryTelegramUserID))
return nil, err
}
return &TreasuryTelegramUsers{
logger: logger,
repo: repo,
}, nil
}
func (t *TreasuryTelegramUsers) FindByTelegramUserID(ctx context.Context, telegramUserID string) (*model.TreasuryTelegramUser, error) {
telegramUserID = strings.TrimSpace(telegramUserID)
if telegramUserID == "" {
return nil, merrors.InvalidArgument("telegram_user_id is required", "telegram_user_id")
}
var result model.TreasuryTelegramUser
err := t.repo.FindOneByFilter(ctx, repository.Filter(fieldTreasuryTelegramUserID, telegramUserID), &result)
if errors.Is(err, merrors.ErrNoData) {
return nil, nil
}
if err != nil {
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
t.logger.Warn("Failed to load treasury telegram user", zap.Error(err), zap.String("telegram_user_id", telegramUserID))
}
return nil, err
}
result.TelegramUserID = strings.TrimSpace(result.TelegramUserID)
result.LedgerAccountID = strings.TrimSpace(result.LedgerAccountID)
if len(result.AllowedChatIDs) > 0 {
normalized := make([]string, 0, len(result.AllowedChatIDs))
for _, next := range result.AllowedChatIDs {
next = strings.TrimSpace(next)
if next == "" {
continue
}
normalized = append(normalized, next)
}
result.AllowedChatIDs = normalized
}
if result.TelegramUserID == "" || result.LedgerAccountID == "" {
return nil, nil
}
return &result, nil
}
var _ storage.TreasuryTelegramUsersStore = (*TreasuryTelegramUsers)(nil)

View File

@@ -23,6 +23,7 @@ class PaymentIntentDTO {
final String? feeTreatment; final String? feeTreatment;
final Map<String, String>? attributes; final Map<String, String>? attributes;
final String? comment;
final CustomerDTO? customer; final CustomerDTO? customer;
const PaymentIntentDTO({ const PaymentIntentDTO({
@@ -33,10 +34,12 @@ class PaymentIntentDTO {
this.fx, this.fx,
this.settlementMode, this.settlementMode,
this.attributes, this.attributes,
this.comment,
this.customer, this.customer,
this.feeTreatment, this.feeTreatment,
}); });
factory PaymentIntentDTO.fromJson(Map<String, dynamic> json) => _$PaymentIntentDTOFromJson(json); factory PaymentIntentDTO.fromJson(Map<String, dynamic> json) =>
_$PaymentIntentDTOFromJson(json);
Map<String, dynamic> toJson() => _$PaymentIntentDTOToJson(this); Map<String, dynamic> toJson() => _$PaymentIntentDTOToJson(this);
} }

View File

@@ -1,11 +1,11 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/payment/operation.dart'; import 'package:pshared/data/dto/payment/operation.dart';
import 'package:pshared/data/dto/payment/intent/payment.dart';
import 'package:pshared/data/dto/payment/payment_quote.dart'; import 'package:pshared/data/dto/payment/payment_quote.dart';
part 'payment.g.dart'; part 'payment.g.dart';
@JsonSerializable() @JsonSerializable()
class PaymentDTO { class PaymentDTO {
final String? paymentRef; final String? paymentRef;
@@ -13,7 +13,6 @@ class PaymentDTO {
final String? state; final String? state;
final String? failureCode; final String? failureCode;
final String? failureReason; final String? failureReason;
final PaymentIntentDTO? intent;
final List<PaymentOperationDTO> operations; final List<PaymentOperationDTO> operations;
final PaymentQuoteDTO? lastQuote; final PaymentQuoteDTO? lastQuote;
final Map<String, String>? metadata; final Map<String, String>? metadata;
@@ -25,7 +24,6 @@ class PaymentDTO {
this.state, this.state,
this.failureCode, this.failureCode,
this.failureReason, this.failureReason,
this.intent,
this.operations = const <PaymentOperationDTO>[], this.operations = const <PaymentOperationDTO>[],
this.lastQuote, this.lastQuote,
this.metadata, this.metadata,

View File

@@ -16,6 +16,7 @@ extension PaymentIntentMapper on PaymentIntent {
fx: fx?.toDTO(), fx: fx?.toDTO(),
settlementMode: settlementModeToValue(settlementMode), settlementMode: settlementModeToValue(settlementMode),
attributes: attributes, attributes: attributes,
comment: comment,
customer: customer?.toDTO(), customer: customer?.toDTO(),
feeTreatment: feeTreatmentToValue(feeTreatment), feeTreatment: feeTreatmentToValue(feeTreatment),
); );
@@ -30,6 +31,7 @@ extension PaymentIntentDTOMapper on PaymentIntentDTO {
fx: fx?.toDomain(), fx: fx?.toDomain(),
settlementMode: settlementModeFromValue(settlementMode), settlementMode: settlementModeFromValue(settlementMode),
attributes: attributes, attributes: attributes,
comment: comment,
customer: customer?.toDomain(), customer: customer?.toDomain(),
feeTreatment: feeTreatmentFromValue(feeTreatment), feeTreatment: feeTreatmentFromValue(feeTreatment),
); );

View File

@@ -1,10 +1,10 @@
import 'package:pshared/data/dto/payment/payment.dart'; import 'package:pshared/data/dto/payment/payment.dart';
import 'package:pshared/data/mapper/payment/intent/payment.dart';
import 'package:pshared/data/mapper/payment/operation.dart'; import 'package:pshared/data/mapper/payment/operation.dart';
import 'package:pshared/data/mapper/payment/quote.dart'; import 'package:pshared/data/mapper/payment/quote.dart';
import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/state.dart'; import 'package:pshared/models/payment/state.dart';
extension PaymentDTOMapper on PaymentDTO { extension PaymentDTOMapper on PaymentDTO {
Payment toDomain() => Payment( Payment toDomain() => Payment(
paymentRef: paymentRef, paymentRef: paymentRef,
@@ -13,7 +13,6 @@ extension PaymentDTOMapper on PaymentDTO {
orchestrationState: paymentOrchestrationStateFromValue(state), orchestrationState: paymentOrchestrationStateFromValue(state),
failureCode: failureCode, failureCode: failureCode,
failureReason: failureReason, failureReason: failureReason,
intent: intent?.toDomain(),
operations: operations.map((item) => item.toDomain()).toList(), operations: operations.map((item) => item.toDomain()).toList(),
lastQuote: lastQuote?.toDomain(), lastQuote: lastQuote?.toDomain(),
metadata: metadata, metadata: metadata,
@@ -28,7 +27,6 @@ extension PaymentMapper on Payment {
state: state ?? paymentOrchestrationStateToValue(orchestrationState), state: state ?? paymentOrchestrationStateToValue(orchestrationState),
failureCode: failureCode, failureCode: failureCode,
failureReason: failureReason, failureReason: failureReason,
intent: intent?.toDTO(),
operations: operations.map((item) => item.toDTO()).toList(), operations: operations.map((item) => item.toDTO()).toList(),
lastQuote: lastQuote?.toDTO(), lastQuote: lastQuote?.toDTO(),
metadata: metadata, metadata: metadata,

View File

@@ -17,6 +17,7 @@ class PaymentIntent {
final FeeTreatment feeTreatment; final FeeTreatment feeTreatment;
final SettlementMode settlementMode; final SettlementMode settlementMode;
final Map<String, String>? attributes; final Map<String, String>? attributes;
final String? comment;
final Customer? customer; final Customer? customer;
const PaymentIntent({ const PaymentIntent({
@@ -29,6 +30,7 @@ class PaymentIntent {
this.fx, this.fx,
this.settlementMode = SettlementMode.unspecified, this.settlementMode = SettlementMode.unspecified,
this.attributes, this.attributes,
this.comment,
this.customer, this.customer,
required this.feeTreatment, required this.feeTreatment,
}); });

View File

@@ -1,5 +1,4 @@
import 'package:pshared/models/payment/execution_operation.dart'; import 'package:pshared/models/payment/execution_operation.dart';
import 'package:pshared/models/payment/intent.dart';
import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/quote/quote.dart';
import 'package:pshared/models/payment/state.dart'; import 'package:pshared/models/payment/state.dart';
@@ -10,7 +9,6 @@ class Payment {
final PaymentOrchestrationState orchestrationState; final PaymentOrchestrationState orchestrationState;
final String? failureCode; final String? failureCode;
final String? failureReason; final String? failureReason;
final PaymentIntent? intent;
final List<PaymentExecutionOperation> operations; final List<PaymentExecutionOperation> operations;
final PaymentQuote? lastQuote; final PaymentQuote? lastQuote;
final Map<String, String>? metadata; final Map<String, String>? metadata;
@@ -23,7 +21,6 @@ class Payment {
required this.orchestrationState, required this.orchestrationState,
required this.failureCode, required this.failureCode,
required this.failureReason, required this.failureReason,
this.intent,
required this.operations, required this.operations,
required this.lastQuote, required this.lastQuote,
required this.metadata, required this.metadata,

View File

@@ -57,6 +57,7 @@ void main() {
), ),
amount: MoneyDTO(amount: '10', currency: 'USD'), amount: MoneyDTO(amount: '10', currency: 'USD'),
settlementMode: 'fix_received', settlementMode: 'fix_received',
comment: 'invoice-7',
), ),
); );
@@ -70,6 +71,7 @@ void main() {
final intent = json['intent'] as Map<String, dynamic>; final intent = json['intent'] as Map<String, dynamic>;
expect(intent['kind'], equals('payout')); expect(intent['kind'], equals('payout'));
expect(intent['settlement_mode'], equals('fix_received')); expect(intent['settlement_mode'], equals('fix_received'));
expect(intent['comment'], equals('invoice-7'));
expect(intent.containsKey('settlement_currency'), isFalse); expect(intent.containsKey('settlement_currency'), isFalse);
final source = intent['source'] as Map<String, dynamic>; final source = intent['source'] as Map<String, dynamic>;

View File

@@ -25,7 +25,6 @@ class PayoutRoutes {
static const walletTopUp = 'payout-wallet-top-up'; static const walletTopUp = 'payout-wallet-top-up';
static const paymentTypeQuery = 'paymentType'; static const paymentTypeQuery = 'paymentType';
static const destinationLedgerAccountRefQuery = 'destinationLedgerAccountRef';
static const reportPaymentIdQuery = 'paymentId'; static const reportPaymentIdQuery = 'paymentId';
static const dashboardPath = '/dashboard'; static const dashboardPath = '/dashboard';
@@ -41,6 +40,7 @@ class PayoutRoutes {
static const editWalletPath = '/methods/edit'; static const editWalletPath = '/methods/edit';
static const walletTopUpPath = '/wallet/top-up'; static const walletTopUpPath = '/wallet/top-up';
static String nameFor(PayoutDestination destination) { static String nameFor(PayoutDestination destination) {
switch (destination) { switch (destination) {
case PayoutDestination.dashboard: case PayoutDestination.dashboard:
@@ -126,13 +126,9 @@ class PayoutRoutes {
static Map<String, String> buildQueryParameters({ static Map<String, String> buildQueryParameters({
PaymentType? paymentType, PaymentType? paymentType,
String? destinationLedgerAccountRef,
}) { }) {
final params = <String, String>{ final params = <String, String>{
if (paymentType != null) paymentTypeQuery: paymentType.name, if (paymentType != null) paymentTypeQuery: paymentType.name,
if (destinationLedgerAccountRef != null &&
destinationLedgerAccountRef.trim().isNotEmpty)
destinationLedgerAccountRefQuery: destinationLedgerAccountRef.trim(),
}; };
return params; return params;
} }
@@ -144,44 +140,35 @@ class PayoutRoutes {
? null ? null
: PaymentType.values.firstWhereOrNull((type) => type.name == raw); : PaymentType.values.firstWhereOrNull((type) => type.name == raw);
static String? destinationLedgerAccountRefFromState(GoRouterState state) =>
destinationLedgerAccountRefFromRaw(
state.uri.queryParameters[destinationLedgerAccountRefQuery],
);
static String? destinationLedgerAccountRefFromRaw(String? raw) {
final value = raw?.trim();
if (value == null || value.isEmpty) return null;
return value;
}
} }
extension PayoutNavigation on BuildContext { extension PayoutNavigation on BuildContext {
void goToPayout(PayoutDestination destination) => void goToPayout(PayoutDestination destination) => goNamed(PayoutRoutes.nameFor(destination));
goNamed(PayoutRoutes.nameFor(destination));
void pushToPayout(PayoutDestination destination) => void pushToPayout(PayoutDestination destination) => pushNamed(PayoutRoutes.nameFor(destination));
pushNamed(PayoutRoutes.nameFor(destination));
void goToPayment({ void goToPayment({
PaymentType? paymentType, PaymentType? paymentType,
String? destinationLedgerAccountRef, }) =>
}) => goNamed( goNamed(
PayoutRoutes.payment, PayoutRoutes.payment,
queryParameters: PayoutRoutes.buildQueryParameters( queryParameters: PayoutRoutes.buildQueryParameters(
paymentType: paymentType, paymentType: paymentType,
destinationLedgerAccountRef: destinationLedgerAccountRef,
), ),
); );
void goToReportPayment(String paymentId) => goNamed( void goToReportPayment(String paymentId) => goNamed(
PayoutRoutes.reportPayment, PayoutRoutes.reportPayment,
queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId}, queryParameters: {
PayoutRoutes.reportPaymentIdQuery: paymentId,
},
); );
void pushToReportPayment(String paymentId) => pushNamed( void pushToReportPayment(String paymentId) => pushNamed(
PayoutRoutes.reportPayment, PayoutRoutes.reportPayment,
queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId}, queryParameters: {
PayoutRoutes.reportPaymentIdQuery: paymentId,
},
); );
void pushToWalletTopUp() => pushNamed(PayoutRoutes.walletTopUp); void pushToWalletTopUp() => pushNamed(PayoutRoutes.walletTopUp);

View File

@@ -6,7 +6,6 @@ import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/controllers/payment/source.dart';
import 'package:pshared/models/ledger/account.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/ledger.dart'; import 'package:pshared/provider/ledger.dart';
@@ -228,9 +227,7 @@ RouteBase payoutShellRoute() => ShellRoute(
onGoToPaymentWithoutRecipient: (type) => onGoToPaymentWithoutRecipient: (type) =>
_startPayment(context, recipient: null, paymentType: type), _startPayment(context, recipient: null, paymentType: type),
onTopUp: (wallet) => _openWalletTopUp(context, wallet), onTopUp: (wallet) => _openWalletTopUp(context, wallet),
onLedgerAddFunds: (account) => _openLedgerAddFunds(context, account),
onWalletTap: (wallet) => _openWalletEdit(context, wallet), onWalletTap: (wallet) => _openWalletEdit(context, wallet),
onLedgerTap: (account) => _openLedgerEdit(context, account),
), ),
), ),
), ),
@@ -307,8 +304,6 @@ RouteBase payoutShellRoute() => ShellRoute(
child: PaymentPage( child: PaymentPage(
onBack: (_) => _popOrGo(context), onBack: (_) => _popOrGo(context),
initialPaymentType: PayoutRoutes.paymentTypeFromState(state), initialPaymentType: PayoutRoutes.paymentTypeFromState(state),
initialDestinationLedgerAccountRef:
PayoutRoutes.destinationLedgerAccountRefFromState(state),
fallbackDestination: fallbackDestination, fallbackDestination: fallbackDestination,
), ),
); );
@@ -345,9 +340,17 @@ RouteBase payoutShellRoute() => ShellRoute(
GoRoute( GoRoute(
name: PayoutRoutes.editWallet, name: PayoutRoutes.editWallet,
path: PayoutRoutes.editWalletPath, path: PayoutRoutes.editWalletPath,
pageBuilder: (context, state) => NoTransitionPage( pageBuilder: (context, state) {
child: WalletEditPage(onBack: () => _popOrGo(context)), final walletsProvider = context.read<WalletsController>();
), final wallet = walletsProvider.selectedWallet;
final loc = AppLocalizations.of(context)!;
return NoTransitionPage(
child: wallet != null
? WalletEditPage(onBack: () => _popOrGo(context))
: Center(child: Text(loc.noWalletSelected)),
);
},
), ),
GoRoute( GoRoute(
name: PayoutRoutes.walletTopUp, name: PayoutRoutes.walletTopUp,
@@ -386,32 +389,10 @@ void _openEditRecipient(BuildContext context, {required Recipient recipient}) {
} }
void _openWalletEdit(BuildContext context, Wallet wallet) { void _openWalletEdit(BuildContext context, Wallet wallet) {
context.read<PaymentSourceController>().selectWallet(wallet);
context.read<WalletsController>().selectWallet(wallet); context.read<WalletsController>().selectWallet(wallet);
context.pushToEditWallet(); context.pushToEditWallet();
} }
void _openLedgerEdit(BuildContext context, LedgerAccount account) {
context.read<PaymentSourceController>().selectLedgerByRef(
account.ledgerAccountRef,
);
context.pushToEditWallet();
}
void _openLedgerAddFunds(BuildContext context, LedgerAccount account) {
context.read<PaymentSourceController>().selectLedgerByRef(
account.ledgerAccountRef,
);
context.read<RecipientsProvider>().setCurrentObject(null);
context.pushNamed(
PayoutRoutes.payment,
queryParameters: PayoutRoutes.buildQueryParameters(
paymentType: PaymentType.ledger,
destinationLedgerAccountRef: account.ledgerAccountRef,
),
);
}
void _openWalletTopUp(BuildContext context, Wallet wallet) { void _openWalletTopUp(BuildContext context, Wallet wallet) {
context.read<WalletsController>().selectWallet(wallet); context.read<WalletsController>().selectWallet(wallet);
context.pushToWalletTopUp(); context.pushToWalletTopUp();

View File

@@ -3,24 +3,18 @@ import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/payment/operation.dart'; import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/source_type.dart';
import 'package:pshared/models/payment/status.dart'; import 'package:pshared/models/payment/status.dart';
import 'package:pshared/provider/payment/payments.dart'; import 'package:pshared/provider/payment/payments.dart';
import 'package:pweb/models/state/load_more_state.dart'; import 'package:pweb/models/state/load_more_state.dart';
import 'package:pweb/utils/report/operations/operations.dart'; import 'package:pweb/utils/report/operations/operations.dart';
import 'package:pweb/utils/report/payment_mapper.dart'; import 'package:pweb/utils/report/payment_mapper.dart';
import 'package:pweb/utils/report/source_filter.dart';
class ReportOperationsController extends ChangeNotifier { class ReportOperationsController extends ChangeNotifier {
PaymentsProvider? _payments; PaymentsProvider? _payments;
PaymentSourceType? _sourceType;
Set<String> _sourceRefs = const <String>{};
DateTimeRange? _selectedRange; DateTimeRange? _selectedRange;
final Set<OperationStatus> _selectedStatuses = {}; final Set<OperationStatus> _selectedStatuses = {};
List<Payment> _paymentItems = const [];
List<OperationItem> _operations = const []; List<OperationItem> _operations = const [];
List<OperationItem> _filtered = const []; List<OperationItem> _filtered = const [];
@@ -42,20 +36,10 @@ class ReportOperationsController extends ChangeNotifier {
return LoadMoreState.hidden; return LoadMoreState.hidden;
} }
void update( void update(PaymentsProvider provider) {
PaymentsProvider provider, {
PaymentSourceType? sourceType,
String? sourceRef,
List<String>? sourceRefs,
}) {
if (!identical(_payments, provider)) { if (!identical(_payments, provider)) {
_payments = provider; _payments = provider;
} }
_sourceType = sourceType;
final effectiveSourceRefs =
sourceRefs ??
(sourceRef == null ? const <String>[] : <String>[sourceRef]);
_sourceRefs = _normalizeRefs(effectiveSourceRefs);
_rebuildOperations(); _rebuildOperations();
} }
@@ -90,16 +74,13 @@ class ReportOperationsController extends ChangeNotifier {
} }
void _rebuildOperations() { void _rebuildOperations() {
_paymentItems = _payments?.payments ?? const []; final items = _payments?.payments ?? const [];
_operations = _paymentItems _operations = items.map(mapPaymentToOperation).toList();
.where(_matchesCurrentSource)
.map(mapPaymentToOperation)
.toList();
_rebuildFiltered(notify: true); _rebuildFiltered(notify: true);
} }
void _rebuildFiltered({bool notify = true}) { void _rebuildFiltered({bool notify = true}) {
_filtered = _applyFilters(sortOperations(_operations)); _filtered = _applyFilters(_operations);
if (notify) { if (notify) {
notifyListeners(); notifyListeners();
} }
@@ -107,14 +88,13 @@ class ReportOperationsController extends ChangeNotifier {
List<OperationItem> _applyFilters(List<OperationItem> operations) { List<OperationItem> _applyFilters(List<OperationItem> operations) {
if (_selectedRange == null && _selectedStatuses.isEmpty) { if (_selectedRange == null && _selectedStatuses.isEmpty) {
return operations; return sortOperations(operations);
} }
final filtered = operations.where((op) { final filtered = operations.where((op) {
final statusMatch = final statusMatch =
_selectedStatuses.isEmpty || _selectedStatuses.contains(op.status); _selectedStatuses.isEmpty || _selectedStatuses.contains(op.status);
final dateMatch = final dateMatch = _selectedRange == null ||
_selectedRange == null ||
isUnknownDate(op.date) || isUnknownDate(op.date) ||
(op.date.isAfter( (op.date.isAfter(
_selectedRange!.start.subtract(const Duration(seconds: 1)), _selectedRange!.start.subtract(const Duration(seconds: 1)),
@@ -125,30 +105,7 @@ class ReportOperationsController extends ChangeNotifier {
return statusMatch && dateMatch; return statusMatch && dateMatch;
}).toList(); }).toList();
return filtered; return sortOperations(filtered);
}
bool _matchesCurrentSource(Payment payment) {
final sourceType = _sourceType;
if (sourceType == null || _sourceRefs.isEmpty) return true;
for (final sourceRef in _sourceRefs) {
if (paymentMatchesSource(
payment,
sourceType: sourceType,
sourceRef: sourceRef,
)) {
return true;
}
}
return false;
}
Set<String> _normalizeRefs(List<String> refs) {
final normalized = refs
.map((value) => value.trim())
.where((value) => value.isNotEmpty)
.toSet();
return normalized;
} }
bool _isSameRange(DateTimeRange? left, DateTimeRange? right) { bool _isSameRange(DateTimeRange? left, DateTimeRange? right) {

View File

@@ -71,24 +71,16 @@ class WalletTransactionsController extends ChangeNotifier {
void _rebuildFiltered({bool notify = true}) { void _rebuildFiltered({bool notify = true}) {
final source = _provider?.transactions ?? const <WalletTransaction>[]; final source = _provider?.transactions ?? const <WalletTransaction>[];
final activeWalletId = _provider?.walletId;
_filteredTransactions = source.where((tx) { _filteredTransactions = source.where((tx) {
final walletMatch =
activeWalletId == null || tx.walletId == activeWalletId;
final statusMatch = final statusMatch =
_selectedStatuses.isEmpty || _selectedStatuses.contains(tx.status); _selectedStatuses.isEmpty || _selectedStatuses.contains(tx.status);
final typeMatch = final typeMatch =
_selectedTypes.isEmpty || _selectedTypes.contains(tx.type); _selectedTypes.isEmpty || _selectedTypes.contains(tx.type);
final dateMatch = final dateMatch = _dateRange == null ||
_dateRange == null || (tx.date.isAfter(_dateRange!.start.subtract(const Duration(seconds: 1))) &&
(tx.date.isAfter( tx.date.isBefore(_dateRange!.end.add(const Duration(seconds: 1))));
_dateRange!.start.subtract(const Duration(seconds: 1)),
) &&
tx.date.isBefore(
_dateRange!.end.add(const Duration(seconds: 1)),
));
return walletMatch && statusMatch && typeMatch && dateMatch; return statusMatch && typeMatch && dateMatch;
}).toList(); }).toList();
if (notify) notifyListeners(); if (notify) notifyListeners();

View File

@@ -1,12 +1,13 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:pshared/models/payment/operation_document.dart';
import 'package:pshared/models/payment/execution_operation.dart'; import 'package:pshared/models/payment/execution_operation.dart';
import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pshared/provider/payment/payments.dart'; import 'package:pshared/provider/payment/payments.dart';
import 'package:pweb/models/documents/operation.dart';
import 'package:pweb/utils/report/operations/document_rule.dart'; import 'package:pweb/utils/report/operations/document_rule.dart';
import 'package:pweb/utils/report/payment_mapper.dart';
class PaymentDetailsController extends ChangeNotifier { class PaymentDetailsController extends ChangeNotifier {
PaymentDetailsController({required String paymentId}) PaymentDetailsController({required String paymentId})
@@ -21,7 +22,26 @@ class PaymentDetailsController extends ChangeNotifier {
bool get isLoading => _payments?.isLoading ?? false; bool get isLoading => _payments?.isLoading ?? false;
Exception? get error => _payments?.error; Exception? get error => _payments?.error;
OperationDocumentRef? operationDocumentRequest( bool get canDownload {
final current = _payment;
if (current == null) return false;
if (statusFromPayment(current) != OperationStatus.success) return false;
return primaryOperationDocumentRequest != null;
}
OperationDocumentRequestModel? get primaryOperationDocumentRequest {
final current = _payment;
if (current == null) return null;
for (final operation in current.operations) {
final request = operationDocumentRequest(operation);
if (request != null) {
return request;
}
}
return null;
}
OperationDocumentRequestModel? operationDocumentRequest(
PaymentExecutionOperation operation, PaymentExecutionOperation operation,
) { ) {
final current = _payment; final current = _payment;
@@ -34,7 +54,7 @@ class PaymentDetailsController extends ChangeNotifier {
if (!isOperationDocumentEligible(operation.code)) return null; if (!isOperationDocumentEligible(operation.code)) return null;
return OperationDocumentRef( return OperationDocumentRequestModel(
gatewayService: gatewayService, gatewayService: gatewayService,
operationRef: operationRef, operationRef: operationRef,
); );

View File

@@ -638,7 +638,7 @@
} }
} }
}, },
"noFee": "None", "noFee": "No fee",
"recipientWillReceive": "Recipient will receive: {amount}", "recipientWillReceive": "Recipient will receive: {amount}",
"@recipientWillReceive": { "@recipientWillReceive": {

View File

@@ -638,7 +638,7 @@
} }
} }
}, },
"noFee": "Без оплаты", "noFee": "Нет комиссии",
"recipientWillReceive": "Получатель получит: {amount}", "recipientWillReceive": "Получатель получит: {amount}",
"@recipientWillReceive": { "@recipientWillReceive": {

View File

@@ -1,26 +0,0 @@
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/payment/wallet.dart';
sealed class BalanceItem {
const BalanceItem();
const factory BalanceItem.wallet(Wallet wallet) = WalletBalanceItem;
const factory BalanceItem.ledger(LedgerAccount account) = LedgerBalanceItem;
const factory BalanceItem.addAction() = AddBalanceActionItem;
}
final class WalletBalanceItem extends BalanceItem {
final Wallet wallet;
const WalletBalanceItem(this.wallet);
}
final class LedgerBalanceItem extends BalanceItem {
final LedgerAccount account;
const LedgerBalanceItem(this.account);
}
final class AddBalanceActionItem extends BalanceItem {
const AddBalanceActionItem();
}

View File

@@ -0,0 +1,9 @@
class OperationDocumentRequestModel {
final String gatewayService;
final String operationRef;
const OperationDocumentRequestModel({
required this.gatewayService,
required this.operationRef,
});
}

View File

@@ -0,0 +1,27 @@
enum PaymentState {
success,
failed,
cancelled,
processing,
unknown,
}
PaymentState paymentStateFromRaw(String? raw) {
final trimmed = (raw ?? '').trim().toUpperCase();
final normalized = trimmed.startsWith('PAYMENT_STATE_')
? trimmed.substring('PAYMENT_STATE_'.length)
: trimmed;
switch (normalized) {
case 'SUCCESS':
return PaymentState.success;
case 'FAILED':
return PaymentState.failed;
case 'CANCELLED':
return PaymentState.cancelled;
case 'PROCESSING':
return PaymentState.processing;
default:
return PaymentState.unknown;
}
}

View File

@@ -1,9 +1,9 @@
class OperationDocumentRef { class OperationDocumentInfo {
final String gatewayService;
final String operationRef; final String operationRef;
final String gatewayService;
const OperationDocumentRef({ const OperationDocumentInfo({
required this.gatewayService,
required this.operationRef, required this.operationRef,
required this.gatewayService,
}); });
} }

View File

@@ -0,0 +1,21 @@
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/payment/wallet.dart';
enum BalanceItemType { wallet, ledger, addAction }
class BalanceItem {
final BalanceItemType type;
final Wallet? wallet;
final LedgerAccount? account;
const BalanceItem.wallet(this.wallet) : type = BalanceItemType.wallet, account = null;
const BalanceItem.ledger(this.account) : type = BalanceItemType.ledger, wallet = null;
const BalanceItem.addAction() : type = BalanceItemType.addAction, wallet = null, account = null;
bool get isWallet => type == BalanceItemType.wallet;
bool get isLedger => type == BalanceItemType.ledger;
bool get isAdd => type == BalanceItemType.addAction;
}

View File

@@ -1,8 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart'; import 'package:provider/provider.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/utils/l10n/chain.dart';
import 'package:pweb/pages/dashboard/buttons/balance/add_funds.dart';
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
import 'package:pweb/widgets/refresh_balance/wallet.dart';
class WalletCard extends StatelessWidget { class WalletCard extends StatelessWidget {
@@ -19,10 +28,56 @@ class WalletCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BalanceSourceCard.wallet( final networkLabel = (wallet.network == null || wallet.network == ChainNetwork.unspecified)
wallet: wallet, ? null
onTap: onTap, : wallet.network!.localizedName(context);
onAddFunds: onTopUp, final symbol = wallet.tokenSymbol?.trim();
return Card(
color: Theme.of(context).colorScheme.onSecondary,
elevation: WalletCardConfig.elevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
),
child: InkWell(
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
onTap: onTap,
child: SizedBox.expand(
child: Padding(
padding: WalletCardConfig.contentPadding,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BalanceHeader(
title: wallet.name,
subtitle: networkLabel,
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BalanceAmount(
wallet: wallet,
onToggleMask: () {
context.read<WalletsController>().toggleBalanceMask(wallet.id);
},
),
Column(
children: [
WalletBalanceRefreshButton(
walletRef: wallet.id,
),
BalanceAddFunds(onTopUp: onTopUp),
],
),
],
),
],
),
),
),
),
); );
} }
} }

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/models/dashboard/balance_item.dart';
import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart'; import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart';
import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart';
import 'package:pweb/pages/dashboard/buttons/balance/card.dart'; import 'package:pweb/pages/dashboard/buttons/balance/card.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart'; import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart';
@@ -17,9 +16,7 @@ class BalanceCarousel extends StatefulWidget {
final int currentIndex; final int currentIndex;
final ValueChanged<int> onIndexChanged; final ValueChanged<int> onIndexChanged;
final ValueChanged<Wallet> onTopUp; final ValueChanged<Wallet> onTopUp;
final ValueChanged<LedgerAccount> onLedgerAddFunds;
final ValueChanged<Wallet> onWalletTap; final ValueChanged<Wallet> onWalletTap;
final ValueChanged<LedgerAccount> onLedgerTap;
const BalanceCarousel({ const BalanceCarousel({
super.key, super.key,
@@ -27,9 +24,7 @@ class BalanceCarousel extends StatefulWidget {
required this.currentIndex, required this.currentIndex,
required this.onIndexChanged, required this.onIndexChanged,
required this.onTopUp, required this.onTopUp,
required this.onLedgerAddFunds,
required this.onWalletTap, required this.onWalletTap,
required this.onLedgerTap,
}); });
@override @override
@@ -104,18 +99,14 @@ class _BalanceCarouselState extends State<BalanceCarousel> {
itemCount: widget.items.length, itemCount: widget.items.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = widget.items[index]; final item = widget.items[index];
final Widget card = switch (item) { final Widget card = switch (item.type) {
WalletBalanceItem(:final wallet) => WalletCard( BalanceItemType.wallet => WalletCard(
wallet: wallet, wallet: item.wallet!,
onTopUp: () => widget.onTopUp(wallet), onTopUp: () => widget.onTopUp(item.wallet!),
onTap: () => widget.onWalletTap(wallet), onTap: () => widget.onWalletTap(item.wallet!),
), ),
LedgerBalanceItem(:final account) => LedgerAccountCard( BalanceItemType.ledger => LedgerAccountCard(account: item.account!),
account: account, BalanceItemType.addAction => const AddBalanceCard(),
onTap: () => widget.onLedgerTap(account),
onAddFunds: () => widget.onLedgerAddFunds(account),
),
AddBalanceActionItem() => const AddBalanceCard(),
}; };
return Padding( return Padding(
@@ -132,16 +123,19 @@ class _BalanceCarouselState extends State<BalanceCarousel> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton( IconButton(
onPressed: safeIndex > 0 ? () => _goToPage(safeIndex - 1) : null, onPressed: safeIndex > 0
? () => _goToPage(safeIndex - 1)
: null,
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
CarouselIndicator(itemCount: widget.items.length, index: safeIndex), CarouselIndicator(
itemCount: widget.items.length,
index: safeIndex,
),
const SizedBox(width: 16), const SizedBox(width: 16),
IconButton( IconButton(
onPressed: safeIndex < widget.items.length - 1 onPressed: safeIndex < widget.items.length - 1 ? () => _goToPage(safeIndex + 1) : null,
? () => _goToPage(safeIndex + 1)
: null,
icon: const Icon(Icons.arrow_forward), icon: const Icon(Icons.arrow_forward),
), ),
], ],

View File

@@ -3,8 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/provider/ledger.dart'; import 'package:pshared/provider/ledger.dart';
import 'package:pweb/models/dashboard/balance_item.dart'; import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart';
class BalanceCarouselController with ChangeNotifier { class BalanceCarouselController with ChangeNotifier {
WalletsController? _walletsController; WalletsController? _walletsController;
@@ -74,19 +73,14 @@ class BalanceCarouselController with ChangeNotifier {
String? _currentWalletRef(List<BalanceItem> items, int index) { String? _currentWalletRef(List<BalanceItem> items, int index) {
if (items.isEmpty || index < 0 || index >= items.length) return null; if (items.isEmpty || index < 0 || index >= items.length) return null;
final current = items[index]; final current = items[index];
return switch (current) { if (!current.isWallet) return null;
WalletBalanceItem(:final wallet) => wallet.id, return current.wallet?.id;
_ => null,
};
} }
int? _walletIndexByRef(List<BalanceItem> items, String? walletRef) { int? _walletIndexByRef(List<BalanceItem> items, String? walletRef) {
if (walletRef == null || walletRef.isEmpty) return null; if (walletRef == null || walletRef.isEmpty) return null;
final idx = items.indexWhere( final idx = items.indexWhere(
(item) => switch (item) { (item) => item.isWallet && item.wallet?.id == walletRef,
WalletBalanceItem(:final wallet) => wallet.id == walletRef,
_ => false,
},
); );
if (idx < 0) return null; if (idx < 0) return null;
return idx; return idx;
@@ -103,17 +97,17 @@ class BalanceCarouselController with ChangeNotifier {
for (var i = 0; i < left.length; i++) { for (var i = 0; i < left.length; i++) {
final a = left[i]; final a = left[i];
final b = right[i]; final b = right[i];
if (a.runtimeType != b.runtimeType) return false; if (a.type != b.type) return false;
if (_itemIdentity(a) != _itemIdentity(b)) return false; if (_itemIdentity(a) != _itemIdentity(b)) return false;
} }
return true; return true;
} }
String _itemIdentity(BalanceItem item) => switch (item) { String _itemIdentity(BalanceItem item) => switch (item.type) {
WalletBalanceItem(:final wallet) => wallet.id, BalanceItemType.wallet => item.wallet?.id ?? '',
LedgerBalanceItem(:final account) => account.ledgerAccountRef, BalanceItemType.ledger => item.account?.ledgerAccountRef ?? '',
AddBalanceActionItem() => 'add', BalanceItemType.addAction => 'add',
}; };
void _syncSelectedWallet() { void _syncSelectedWallet() {
@@ -121,8 +115,9 @@ class BalanceCarouselController with ChangeNotifier {
if (walletsController == null || _items.isEmpty) return; if (walletsController == null || _items.isEmpty) return;
final current = _items[_index]; final current = _items[_index];
if (current is! WalletBalanceItem) return; if (!current.isWallet || current.wallet == null) return;
final wallet = current.wallet;
final wallet = current.wallet!;
if (walletsController.selectedWallet?.id == wallet.id) return; if (walletsController.selectedWallet?.id == wallet.id) return;
walletsController.selectWallet(wallet); walletsController.selectWallet(wallet);
} }

View File

@@ -1,27 +1,124 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/ledger_accounts.dart';
import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/ledger/account.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart'; import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
import 'package:pweb/widgets/refresh_balance/ledger.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class LedgerAccountCard extends StatelessWidget { class LedgerAccountCard extends StatelessWidget {
final LedgerAccount account; final LedgerAccount account;
final VoidCallback onAddFunds;
final VoidCallback? onTap;
const LedgerAccountCard({ const LedgerAccountCard({super.key, required this.account});
super.key,
required this.account, String _formatBalance() {
required this.onAddFunds, final money = account.balance?.balance;
this.onTap, if (money == null) return '--';
});
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
if (amount.isNaN) {
return '${money.amount} ${money.currency}';
}
try {
final currency = currencyStringToCode(money.currency);
final symbol = currencyCodeToSymbol(currency);
if (symbol.trim().isEmpty) {
return '${amountToString(amount)} ${money.currency}';
}
return '${amountToString(amount)} $symbol';
} catch (_) {
return '${amountToString(amount)} ${money.currency}';
}
}
String _formatMaskedBalance() {
final currency = account.currency.trim();
if (currency.isEmpty) return '••••';
try {
final symbol = currencyCodeToSymbol(currencyStringToCode(currency));
if (symbol.trim().isEmpty) {
return '•••• $currency';
}
return '•••• $symbol';
} catch (_) {
return '•••• $currency';
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BalanceSourceCard.ledger( final textTheme = Theme.of(context).textTheme;
account: account, final colorScheme = Theme.of(context).colorScheme;
onTap: onTap ?? () {}, final loc = AppLocalizations.of(context)!;
onAddFunds: onAddFunds, final accountName = account.name.trim();
final accountCode = account.accountCode.trim();
final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger;
final subtitle = accountCode.isNotEmpty ? accountCode : null;
final badge = account.currency.trim().isEmpty
? null
: account.currency.toUpperCase();
return Card(
color: colorScheme.onSecondary,
elevation: WalletCardConfig.elevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
),
child: Padding(
padding: WalletCardConfig.contentPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BalanceHeader(title: title, subtitle: subtitle, badge: badge),
Row(
children: [
Consumer<LedgerBalanceMaskController>(
builder: (context, controller, _) {
final isMasked = controller.isBalanceMasked(
account.ledgerAccountRef,
);
return Row(
children: [
Text(
isMasked ? _formatMaskedBalance() : _formatBalance(),
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () => controller.toggleBalanceMask(
account.ledgerAccountRef,
),
child: Icon(
isMasked ? Icons.visibility_off : Icons.visibility,
size: 24,
color: colorScheme.onSurface,
),
),
],
);
},
),
const SizedBox(width: 12),
LedgerBalanceRefreshButton(
ledgerAccountRef: account.ledgerAccountRef,
),
],
),
],
),
),
); );
} }
} }

View File

@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/ledger_accounts.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart';
class LedgerBalanceAmount extends StatelessWidget {
final LedgerAccount account;
const LedgerBalanceAmount({super.key, required this.account});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
return Consumer<LedgerBalanceMaskController>(
builder: (context, controller, _) {
final isMasked = controller.isBalanceMasked(account.ledgerAccountRef);
final balance = isMasked
? LedgerBalanceFormatter.formatMasked(account)
: LedgerBalanceFormatter.format(account);
return Row(
children: [
Flexible(
child: Text(
balance,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () {
controller.toggleBalanceMask(account.ledgerAccountRef);
},
child: Icon(
isMasked ? Icons.visibility_off : Icons.visibility,
size: 24,
color: colorScheme.onSurface,
),
),
],
);
},
);
}
}

View File

@@ -1,44 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/ledger.dart';
import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class LedgerSourceActions extends StatelessWidget {
final String ledgerAccountRef;
final VoidCallback onAddFunds;
const LedgerSourceActions({
super.key,
required this.ledgerAccountRef,
required this.onAddFunds,
});
@override
Widget build(BuildContext context) {
final ledgerProvider = context.watch<LedgerAccountsProvider>();
final loc = AppLocalizations.of(context)!;
final isBusy =
ledgerProvider.isWalletRefreshing(ledgerAccountRef) ||
ledgerProvider.isLoading;
final hasTarget = ledgerProvider.accounts.any(
(a) => a.ledgerAccountRef == ledgerAccountRef,
);
return BalanceActionsBar(
isRefreshBusy: isBusy,
canRefresh: hasTarget,
onRefresh: () {
context.read<LedgerAccountsProvider>().refreshBalance(ledgerAccountRef);
},
onAddFunds: onAddFunds,
refreshLabel: loc.refreshBalance,
addFundsLabel: loc.addFunds,
);
}
}

View File

@@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/wallets.dart';
import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletSourceActions extends StatelessWidget {
final String walletRef;
final VoidCallback onAddFunds;
const WalletSourceActions({
super.key,
required this.walletRef,
required this.onAddFunds,
});
@override
Widget build(BuildContext context) {
final walletsProvider = context.watch<WalletsProvider>();
final loc = AppLocalizations.of(context)!;
final isBusy =
walletsProvider.isWalletRefreshing(walletRef) ||
walletsProvider.isLoading;
final hasTarget = walletsProvider.wallets.any((w) => w.id == walletRef);
return BalanceActionsBar(
isRefreshBusy: isBusy,
canRefresh: hasTarget,
onRefresh: () {
context.read<WalletsProvider>().refreshBalance(walletRef);
},
onAddFunds: onAddFunds,
refreshLabel: loc.refreshBalance,
addFundsLabel: loc.addFunds,
);
}
}

View File

@@ -1,99 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/models/payment/source_type.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/utils/l10n/chain.dart';
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
import 'package:pweb/pages/dashboard/buttons/balance/ledger_amount.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/actions/ledger.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/actions/wallet.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/card_layout.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class BalanceSourceCard extends StatelessWidget {
final PaymentSourceType _type;
final Wallet? _wallet;
final LedgerAccount? _ledgerAccount;
final VoidCallback onTap;
final VoidCallback onAddFunds;
const BalanceSourceCard.wallet({
super.key,
required Wallet wallet,
required this.onTap,
required this.onAddFunds,
}) : _type = PaymentSourceType.wallet,
_wallet = wallet,
_ledgerAccount = null;
const BalanceSourceCard.ledger({
super.key,
required LedgerAccount account,
required this.onTap,
required this.onAddFunds,
}) : _type = PaymentSourceType.ledger,
_wallet = null,
_ledgerAccount = account;
@override
Widget build(BuildContext context) => switch (_type) {
PaymentSourceType.wallet => _buildWalletCard(context, _wallet!),
PaymentSourceType.ledger => _buildLedgerCard(context, _ledgerAccount!),
};
Widget _buildWalletCard(BuildContext context, Wallet wallet) {
final networkLabel =
(wallet.network == null || wallet.network == ChainNetwork.unspecified)
? null
: wallet.network!.localizedName(context);
final symbol = wallet.tokenSymbol?.trim();
return BalanceSourceCardLayout(
title: wallet.name,
subtitle: networkLabel,
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
onTap: onTap,
actions: WalletSourceActions(
walletRef: wallet.id,
onAddFunds: onAddFunds,
),
amount: BalanceAmount(
wallet: wallet,
onToggleMask: () {
context.read<WalletsController>().toggleBalanceMask(wallet.id);
},
),
);
}
Widget _buildLedgerCard(BuildContext context, LedgerAccount account) {
final loc = AppLocalizations.of(context)!;
final accountName = account.name.trim();
final accountCode = account.accountCode.trim();
final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger;
final subtitle = accountCode.isNotEmpty ? accountCode : null;
final badge = account.currency.trim().isEmpty
? null
: account.currency.toUpperCase();
return BalanceSourceCardLayout(
title: title,
subtitle: subtitle,
badge: badge,
onTap: onTap,
actions: LedgerSourceActions(
ledgerAccountRef: account.ledgerAccountRef,
onAddFunds: onAddFunds,
),
amount: LedgerBalanceAmount(account: account),
);
}
}

View File

@@ -1,61 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
class BalanceSourceCardLayout extends StatelessWidget {
final String title;
final String? subtitle;
final String? badge;
final Widget amount;
final Widget actions;
final VoidCallback onTap;
const BalanceSourceCardLayout({
super.key,
required this.title,
required this.subtitle,
required this.badge,
required this.amount,
required this.actions,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card(
color: colorScheme.onSecondary,
elevation: WalletCardConfig.elevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
),
child: InkWell(
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
onTap: onTap,
child: SizedBox.expand(
child: Padding(
padding: WalletCardConfig.contentPadding,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BalanceHeader(title: title, subtitle: subtitle, badge: badge),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(child: amount),
const SizedBox(width: 12),
actions,
],
),
],
),
),
),
),
);
}
}

View File

@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/provider/ledger.dart'; import 'package:pshared/provider/ledger.dart';
import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/models/payment/wallet.dart';
@@ -14,16 +13,12 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class BalanceWidget extends StatelessWidget { class BalanceWidget extends StatelessWidget {
final ValueChanged<Wallet> onTopUp; final ValueChanged<Wallet> onTopUp;
final ValueChanged<LedgerAccount> onLedgerAddFunds;
final ValueChanged<Wallet> onWalletTap; final ValueChanged<Wallet> onWalletTap;
final ValueChanged<LedgerAccount> onLedgerTap;
const BalanceWidget({ const BalanceWidget({
super.key, super.key,
required this.onTopUp, required this.onTopUp,
required this.onLedgerAddFunds,
required this.onWalletTap, required this.onWalletTap,
required this.onLedgerTap,
}); });
@override @override
@@ -50,9 +45,7 @@ class BalanceWidget extends StatelessWidget {
currentIndex: carousel.index, currentIndex: carousel.index,
onIndexChanged: carousel.onPageChanged, onIndexChanged: carousel.onPageChanged,
onTopUp: onTopUp, onTopUp: onTopUp,
onLedgerAddFunds: onLedgerAddFunds,
onWalletTap: onWalletTap, onWalletTap: onWalletTap,
onLedgerTap: onLedgerTap,
); );
if (wallets.isEmpty && accounts.isEmpty) { if (wallets.isEmpty && accounts.isEmpty) {

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/models/payment/wallet.dart';
@@ -16,7 +15,6 @@ import 'package:pweb/pages/loader.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
class AppSpacing { class AppSpacing {
static const double small = 10; static const double small = 10;
static const double medium = 16; static const double medium = 16;
@@ -27,18 +25,14 @@ class DashboardPage extends StatefulWidget {
final ValueChanged<Recipient> onRecipientSelected; final ValueChanged<Recipient> onRecipientSelected;
final void Function(PaymentType type) onGoToPaymentWithoutRecipient; final void Function(PaymentType type) onGoToPaymentWithoutRecipient;
final ValueChanged<Wallet> onTopUp; final ValueChanged<Wallet> onTopUp;
final ValueChanged<LedgerAccount> onLedgerAddFunds;
final ValueChanged<Wallet> onWalletTap; final ValueChanged<Wallet> onWalletTap;
final ValueChanged<LedgerAccount> onLedgerTap;
const DashboardPage({ const DashboardPage({
super.key, super.key,
required this.onRecipientSelected, required this.onRecipientSelected,
required this.onGoToPaymentWithoutRecipient, required this.onGoToPaymentWithoutRecipient,
required this.onTopUp, required this.onTopUp,
required this.onLedgerAddFunds,
required this.onWalletTap, required this.onWalletTap,
required this.onLedgerTap,
}); });
@override @override
@@ -92,9 +86,7 @@ class _DashboardPageState extends State<DashboardPage> {
BalanceWidgetProviders( BalanceWidgetProviders(
child: BalanceWidget( child: BalanceWidget(
onTopUp: widget.onTopUp, onTopUp: widget.onTopUp,
onLedgerAddFunds: widget.onLedgerAddFunds,
onWalletTap: widget.onWalletTap, onWalletTap: widget.onWalletTap,
onLedgerTap: widget.onLedgerTap,
), ),
), ),
const SizedBox(height: AppSpacing.small), const SizedBox(height: AppSpacing.small),

View File

@@ -5,11 +5,11 @@ import 'package:provider/provider.dart';
import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/currency.dart';
import 'package:pweb/controllers/payments/amount_field.dart'; import 'package:pweb/controllers/payments/amount_field.dart';
import 'package:pweb/models/payment/amount/mode.dart';
import 'package:pweb/pages/dashboard/payouts/amount/mode/selector.dart'; import 'package:pweb/pages/dashboard/payouts/amount/mode/selector.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentAmountField extends StatelessWidget { class PaymentAmountField extends StatelessWidget {
const PaymentAmountField(); const PaymentAmountField();
@@ -37,6 +37,10 @@ class PaymentAmountField extends StatelessWidget {
labelText: loc.amount, labelText: loc.amount,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
prefixText: symbol == null ? null : '$symbol\u00A0', prefixText: symbol == null ? null : '$symbol\u00A0',
helperText: switch (ui.mode) {
PaymentAmountMode.debit => loc.debitAmountLabel,
PaymentAmountMode.settlement => loc.expectedSettlementAmountLabel,
},
), ),
onChanged: ui.handleChanged, onChanged: ui.handleChanged,
), ),

View File

@@ -102,7 +102,7 @@ class PaymentFormWidget extends StatelessWidget {
children: [ children: [
detailsHeader, detailsHeader,
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
flex: 3, flex: 3,
@@ -114,7 +114,7 @@ class PaymentFormWidget extends StatelessWidget {
), ),
const SizedBox(height: _smallSpacing), const SizedBox(height: _smallSpacing),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
flex: 3, flex: 3,

View File

@@ -6,7 +6,7 @@ import 'package:pshared/provider/payment/multiple/quotation.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart'; import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/controllers/payouts/payout_verification.dart'; import 'package:pweb/controllers/payouts/payout_verification.dart';
import 'package:pweb/utils/payment/verification_flow.dart'; import 'package:pweb/utils/payment/payout_verification_flow.dart';
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart'; import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';

View File

@@ -26,18 +26,15 @@ class QuoteStatusCard extends StatelessWidget {
}); });
static const double _cardRadius = 12; static const double _cardRadius = 12;
static const double _cardSpacing = 8; static const double _cardSpacing = 12;
static const double _iconSize = 18; static const double _iconSize = 18;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
final foregroundColor = _resolveForegroundColor(theme, statusType); final foregroundColor = _resolveForegroundColor(theme, statusType);
final elementColor = _resolveElementColor(theme, statusType); final elementColor = _resolveElementColor(theme, statusType);
final statusStyle = theme.textTheme.bodyMedium?.copyWith( final statusStyle = theme.textTheme.bodyMedium?.copyWith(color: elementColor);
color: elementColor,
);
final helperStyle = theme.textTheme.bodySmall?.copyWith( final helperStyle = theme.textTheme.bodySmall?.copyWith(
color: foregroundColor.withValues(alpha: 0.8), color: foregroundColor.withValues(alpha: 0.8),
); );
@@ -47,10 +44,12 @@ class QuoteStatusCard extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: _resolveCardColor(theme, statusType), color: _resolveCardColor(theme, statusType),
borderRadius: BorderRadius.circular(_cardRadius), borderRadius: BorderRadius.circular(_cardRadius),
border: Border.all(color: elementColor.withValues(alpha: 0.5)), border: Border.all(
color: elementColor.withValues(alpha: 0.5),
),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(top: 2), padding: const EdgeInsets.only(top: 2),
@@ -60,9 +59,7 @@ class QuoteStatusCard extends StatelessWidget {
height: _iconSize, height: _iconSize,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>( valueColor: AlwaysStoppedAnimation<Color>(foregroundColor),
foregroundColor,
),
), ),
) )
: Icon( : Icon(
@@ -84,15 +81,19 @@ class QuoteStatusCard extends StatelessWidget {
], ],
), ),
), ),
if (canRefresh) ...[ if (canRefresh)
const SizedBox(width: _cardSpacing), Padding(
IconButton( padding: const EdgeInsets.only(left: _cardSpacing),
onPressed: onRefresh, child: showPrimaryRefresh
tooltip: loc.quoteRefresh, ? ElevatedButton(
icon: const Icon(Icons.refresh), onPressed: canRefresh ? onRefresh : null,
color: showPrimaryRefresh ? foregroundColor : elementColor, child: Text(AppLocalizations.of(context)!.quoteRefresh),
)
: TextButton(
onPressed: canRefresh ? onRefresh : null,
child: Text(AppLocalizations.of(context)!.quoteRefresh),
),
), ),
],
], ],
), ),
); );

View File

@@ -23,7 +23,7 @@ class RecipientAvatar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textColor = Theme.of(context).colorScheme.onSecondary; final textColor = Theme.of(context).colorScheme.onPrimary;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -31,7 +31,7 @@ class RecipientAvatar extends StatelessWidget {
CircleAvatar( CircleAvatar(
radius: avatarRadius, radius: avatarRadius,
backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null, backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null,
backgroundColor: Theme.of(context).colorScheme.primaryFixed, backgroundColor: Theme.of(context).colorScheme.primary,
child: avatarUrl == null child: avatarUrl == null
? Text( ? Text(
getInitials(name), getInitials(name),

View File

@@ -7,56 +7,54 @@ import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart';
class ShortListAddressBookPayout extends StatelessWidget { class ShortListAddressBookPayout extends StatelessWidget {
final List<Recipient> recipients; final List<Recipient> recipients;
final ValueChanged<Recipient> onSelected; final ValueChanged<Recipient> onSelected;
final Widget? leading; final Widget? trailing;
const ShortListAddressBookPayout({ const ShortListAddressBookPayout({
super.key, super.key,
required this.recipients, required this.recipients,
required this.onSelected, required this.onSelected,
this.leading, this.trailing,
}); });
static const double _avatarRadius = 20; static const double _avatarRadius = 20;
static const double _avatarSize = 80; static const double _avatarSize = 80;
static const EdgeInsets _padding = EdgeInsets.symmetric( static const EdgeInsets _padding = EdgeInsets.symmetric(horizontal: 10, vertical: 8);
horizontal: 10,
vertical: 8,
);
static const TextStyle _nameStyle = TextStyle(fontSize: 12); static const TextStyle _nameStyle = TextStyle(fontSize: 12);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final leadingWidget = leading; final trailingWidget = trailing;
final recipientItems = recipients.map((recipient) {
return Padding(
padding: _padding,
child: InkWell(
borderRadius: BorderRadius.circular(5),
hoverColor: Theme.of(context).colorScheme.onTertiary,
onTap: () => onSelected(recipient),
child: SizedBox(
height: _avatarSize,
width: _avatarSize,
child: RecipientAvatar(
isVisible: true,
name: recipient.name,
avatarUrl: recipient.avatarUrl,
avatarRadius: _avatarRadius,
nameStyle: _nameStyle,
),
),
),
);
});
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
children: [ children:
if (leadingWidget != null) recipients.map((recipient) {
Padding(padding: _padding, child: leadingWidget), return Padding(
...recipientItems, padding: _padding,
], child: InkWell(
borderRadius: BorderRadius.circular(5),
hoverColor: Theme.of(context).colorScheme.primaryContainer,
onTap: () => onSelected(recipient),
child: SizedBox(
height: _avatarSize,
width: _avatarSize,
child: RecipientAvatar(
isVisible: true,
name: recipient.name,
avatarUrl: recipient.avatarUrl,
avatarRadius: _avatarRadius,
nameStyle: _nameStyle,
),
),
),
);
}).toList()
..addAll(
trailingWidget == null
? const []
: [Padding(padding: _padding, child: trailingWidget)],
),
), ),
); );
} }

View File

@@ -21,7 +21,10 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class AddressBookPayout extends StatefulWidget { class AddressBookPayout extends StatefulWidget {
final ValueChanged<Recipient> onSelected; final ValueChanged<Recipient> onSelected;
const AddressBookPayout({super.key, required this.onSelected}); const AddressBookPayout({
super.key,
required this.onSelected,
});
@override @override
State<AddressBookPayout> createState() => _AddressBookPayoutState(); State<AddressBookPayout> createState() => _AddressBookPayoutState();
@@ -68,7 +71,6 @@ class _AddressBookPayoutState extends State<AddressBookPayout> {
provider.setCurrentObject(null); provider.setCurrentObject(null);
context.pushNamed(PayoutRoutes.addRecipient); context.pushNamed(PayoutRoutes.addRecipient);
} }
final filteredRecipients = filterRecipients( final filteredRecipients = filterRecipients(
recipients: recipients, recipients: recipients,
query: _query, query: _query,
@@ -79,18 +81,16 @@ class _AddressBookPayoutState extends State<AddressBookPayout> {
} }
if (provider.error != null) { if (provider.error != null) {
return Center( return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation)));
child: Text(
loc.notificationError(provider.error ?? loc.noErrorInformation),
),
);
} }
return SizedBox( return SizedBox(
height: _isExpanded ? _expandedHeight : _collapsedHeight, height: _isExpanded ? _expandedHeight : _collapsedHeight,
child: Card( child: Card(
margin: const EdgeInsets.all(_cardMargin), margin: const EdgeInsets.all(_cardMargin),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 4, elevation: 4,
color: Theme.of(context).colorScheme.onSecondary, color: Theme.of(context).colorScheme.onSecondary,
child: Padding( child: Padding(
@@ -105,27 +105,27 @@ class _AddressBookPayoutState extends State<AddressBookPayout> {
const SizedBox(height: _spacingBetween), const SizedBox(height: _spacingBetween),
Expanded( Expanded(
child: recipients.isEmpty child: recipients.isEmpty
? Center( ? Center(
child: AddRecipientTile( child: AddRecipientTile(
label: loc.addRecipient, label: loc.addRecipient,
onTap: onAddRecipient, onTap: onAddRecipient,
), ),
) )
: _isExpanded && filteredRecipients.isEmpty : _isExpanded && filteredRecipients.isEmpty
? AddressBookPlaceholder(text: loc.noRecipientsFound) ? AddressBookPlaceholder(text: loc.noRecipientsFound)
: _isExpanded : _isExpanded
? LongListAddressBookPayout( ? LongListAddressBookPayout(
filteredRecipients: filteredRecipients, filteredRecipients: filteredRecipients,
onSelected: widget.onSelected, onSelected: widget.onSelected,
) )
: ShortListAddressBookPayout( : ShortListAddressBookPayout(
recipients: recipients, recipients: recipients,
onSelected: widget.onSelected, onSelected: widget.onSelected,
leading: AddRecipientTile( trailing: AddRecipientTile(
label: loc.addRecipient, label: loc.addRecipient,
onTap: onAddRecipient, onTap: onAddRecipient,
),
), ),
),
), ),
], ],
), ),

View File

@@ -5,21 +5,19 @@ import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/controllers/payments/page_ui.dart'; import 'package:pweb/controllers/payments/page_ui.dart';
import 'package:pweb/utils/payment/page_handlers.dart'; import 'package:pweb/pages/payout_page/send/page_handlers.dart';
import 'package:pweb/pages/payout_page/send/page_view.dart'; import 'package:pweb/pages/payout_page/send/page_view.dart';
class PaymentPage extends StatefulWidget { class PaymentPage extends StatefulWidget {
final ValueChanged<Recipient?>? onBack; final ValueChanged<Recipient?>? onBack;
final PaymentType? initialPaymentType; final PaymentType? initialPaymentType;
final String? initialDestinationLedgerAccountRef;
final PayoutDestination fallbackDestination; final PayoutDestination fallbackDestination;
const PaymentPage({ const PaymentPage({
super.key, super.key,
this.onBack, this.onBack,
this.initialPaymentType, this.initialPaymentType,
this.initialDestinationLedgerAccountRef,
this.fallbackDestination = PayoutDestination.dashboard, this.fallbackDestination = PayoutDestination.dashboard,
}); });
@@ -36,11 +34,7 @@ class _PaymentPageState extends State<PaymentPage> {
_uiController = PaymentPageUiController(); _uiController = PaymentPageUiController();
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) => initializePaymentPage( (_) => initializePaymentPage(context, widget.initialPaymentType),
context,
widget.initialPaymentType,
destinationLedgerAccountRef: widget.initialDestinationLedgerAccountRef,
),
); );
} }

View File

@@ -4,7 +4,6 @@ import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/ledger.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/payment/flow.dart'; import 'package:pshared/provider/payment/flow.dart';
@@ -18,30 +17,14 @@ import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
import 'package:pweb/controllers/payments/page.dart'; import 'package:pweb/controllers/payments/page.dart';
import 'package:pweb/controllers/payments/page_ui.dart'; import 'package:pweb/controllers/payments/page_ui.dart';
import 'package:pweb/controllers/payouts/payout_verification.dart'; import 'package:pweb/controllers/payouts/payout_verification.dart';
import 'package:pweb/utils/payment/verification_flow.dart'; import 'package:pweb/utils/payment/payout_verification_flow.dart';
void initializePaymentPage( void initializePaymentPage(
BuildContext context, BuildContext context,
PaymentType? initialPaymentType, { PaymentType? initialPaymentType,
String? destinationLedgerAccountRef, ) {
}) {
final flowProvider = context.read<PaymentFlowProvider>(); final flowProvider = context.read<PaymentFlowProvider>();
final recipientsProvider = context.read<RecipientsProvider>();
flowProvider.setPreferredType(initialPaymentType); flowProvider.setPreferredType(initialPaymentType);
final destinationRef = destinationLedgerAccountRef?.trim();
if (destinationRef != null && destinationRef.isNotEmpty) {
recipientsProvider.setCurrentObject(null);
flowProvider.setPreferredType(PaymentType.ledger);
flowProvider.setManualPaymentData(
LedgerPaymentMethod(ledgerAccountRef: destinationRef),
);
return;
}
flowProvider.setManualPaymentData(null);
} }
void handleSearchChanged(PaymentPageUiController uiController, String query) { void handleSearchChanged(PaymentPageUiController uiController, String query) {

View File

@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/payment/flow.dart';
import 'package:pshared/provider/payment/quotation/quotation.dart'; import 'package:pshared/provider/payment/quotation/quotation.dart';
import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/provider.dart';
@@ -15,7 +14,6 @@ import 'package:pweb/controllers/payments/page_ui.dart';
import 'package:pweb/controllers/payouts/payout_verification.dart'; import 'package:pweb/controllers/payouts/payout_verification.dart';
import 'package:pweb/models/state/control_state.dart'; import 'package:pweb/models/state/control_state.dart';
class PaymentPageView extends StatelessWidget { class PaymentPageView extends StatelessWidget {
final PaymentPageUiController uiController; final PaymentPageUiController uiController;
final ValueChanged<Recipient?>? onBack; final ValueChanged<Recipient?>? onBack;
@@ -49,7 +47,6 @@ class PaymentPageView extends StatelessWidget {
final uiController = context.watch<PaymentPageUiController>(); final uiController = context.watch<PaymentPageUiController>();
final methodsProvider = context.watch<PaymentMethodsProvider>(); final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipientProvider = context.watch<RecipientsProvider>(); final recipientProvider = context.watch<RecipientsProvider>();
final flowProvider = context.watch<PaymentFlowProvider>();
final quotationProvider = context.watch<QuotationProvider>(); final quotationProvider = context.watch<QuotationProvider>();
final verificationController = context final verificationController = context
.watch<PayoutVerificationController>(); .watch<PayoutVerificationController>();
@@ -61,12 +58,10 @@ class PaymentPageView extends StatelessWidget {
recipients: recipientProvider.recipients, recipients: recipientProvider.recipients,
query: uiController.query, query: uiController.query,
); );
final hasDestinationSelection =
flowProvider.selectedPaymentData != null;
final sendState = final sendState =
verificationController.isCooldownActiveFor(verificationContextKey) verificationController.isCooldownActiveFor(verificationContextKey)
? ControlState.disabled ? ControlState.disabled
: (!hasDestinationSelection : (recipient == null
? ControlState.disabled ? ControlState.disabled
: ControlState.enabled); : ControlState.enabled);

View File

@@ -1,51 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/utils/dimensions.dart';
class PaymentInfoManualDetailsSection extends StatelessWidget {
final AppDimensions dimensions;
final String title;
final VisibilityState titleVisibility;
final PaymentMethodData data;
const PaymentInfoManualDetailsSection({
super.key,
required this.dimensions,
required this.title,
required this.titleVisibility,
required this.data,
});
@override
Widget build(BuildContext context) {
final entry = RecipientMethodDraft(type: data.type, data: data);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PaymentInfoHeader(
dimensions: dimensions,
title: title,
visibility: titleVisibility,
),
PaymentMethodPanel(
selectedType: data.type,
selectedIndex: 0,
entries: [entry],
onRemove: (_) {},
onChanged: (_, ignored) {},
editState: ControlState.disabled,
deleteVisibility: VisibilityState.hidden,
),
],
);
}
}

View File

@@ -6,7 +6,6 @@ import 'package:pshared/provider/payment/flow.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_section.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_section.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_state.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_state.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/manual_details.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_methods.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_methods.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_recipient.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_recipient.dart';
import 'package:pweb/models/state/visibility.dart'; import 'package:pweb/models/state/visibility.dart';
@@ -36,9 +35,8 @@ class PaymentInfoSection extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
final flowProvider = context.watch<PaymentFlowProvider>(); final flowProvider = context.watch<PaymentFlowProvider>();
final manualData = flowProvider.manualPaymentData;
if (!flowProvider.hasRecipient && manualData == null) { if (!flowProvider.hasRecipient) {
return PaymentInfoNoRecipientSection( return PaymentInfoNoRecipientSection(
dimensions: dimensions, dimensions: dimensions,
title: loc.paymentInfo, title: loc.paymentInfo,
@@ -46,15 +44,6 @@ class PaymentInfoSection extends StatelessWidget {
); );
} }
if (!flowProvider.hasRecipient && manualData != null) {
return PaymentInfoManualDetailsSection(
dimensions: dimensions,
title: loc.paymentInfo,
titleVisibility: titleVisibility,
data: manualData,
);
}
final methods = flowProvider.methodsForRecipient; final methods = flowProvider.methodsForRecipient;
final types = visiblePaymentTypes; final types = visiblePaymentTypes;

View File

@@ -81,7 +81,7 @@ class RecipientSection extends StatelessWidget {
ShortListAddressBookPayout( ShortListAddressBookPayout(
recipients: recipientProvider.recipients, recipients: recipientProvider.recipients,
onSelected: onRecipientSelected, onSelected: onRecipientSelected,
leading: AddRecipientTile( trailing: AddRecipientTile(
label: loc.addRecipient, label: loc.addRecipient,
onTap: onAddRecipient, onTap: onAddRecipient,
), ),

View File

@@ -1,9 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/payment/flow.dart';
import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/section.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/section.dart';
@@ -49,30 +46,24 @@ class PaymentRecipientDetailsCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final flowProvider = context.watch<PaymentFlowProvider>();
final isRecipientSelectionLocked =
!flowProvider.hasRecipient && flowProvider.manualPaymentData != null;
return PaymentSectionCard( return PaymentSectionCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!isRecipientSelectionLocked) ...[ RecipientSection(
RecipientSection( recipient: recipient,
recipient: recipient, dimensions: dimensions,
dimensions: dimensions, recipientProvider: recipientProvider,
recipientProvider: recipientProvider, searchQuery: searchQuery,
searchQuery: searchQuery, filteredRecipients: filteredRecipients,
filteredRecipients: filteredRecipients, searchController: searchController,
searchController: searchController, searchFocusNode: searchFocusNode,
searchFocusNode: searchFocusNode, onSearchChanged: onSearchChanged,
onSearchChanged: onSearchChanged, onRecipientSelected: onRecipientSelected,
onRecipientSelected: onRecipientSelected, onRecipientCleared: onRecipientCleared,
onRecipientCleared: onRecipientCleared, onAddRecipient: onAddRecipient,
onAddRecipient: onAddRecipient, ),
), SizedBox(height: dimensions.paddingMedium),
SizedBox(height: dimensions.paddingMedium),
],
PaymentInfoSection( PaymentInfoSection(
dimensions: dimensions, dimensions: dimensions,
titleVisibility: VisibilityState.hidden, titleVisibility: VisibilityState.hidden,

View File

@@ -2,10 +2,9 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/controllers/payment/source.dart';
import 'package:pweb/pages/payout_page/wallet/edit/buttons/send.dart'; import 'package:pweb/pages/payout_page/wallet/edit/buttons/send.dart';
import 'package:pweb/pages/payout_page/wallet/edit/buttons/top_up.dart'; import 'package:pweb/pages/payout_page/wallet/edit/buttons/top_up.dart';
import 'package:pshared/provider/payment/wallets.dart';
class ButtonsWalletWidget extends StatelessWidget { class ButtonsWalletWidget extends StatelessWidget {
@@ -13,20 +12,25 @@ class ButtonsWalletWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final source = context.watch<PaymentSourceController>(); final provider = context.watch<WalletsProvider>();
if (!source.hasSources) return const SizedBox.shrink();
if (provider.wallets.isEmpty) return const SizedBox.shrink();
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Expanded(child: SendPayoutButton()), Expanded(
VerticalDivider( child: SendPayoutButton(),
color: Theme.of(context).colorScheme.primary, ),
thickness: 1, VerticalDivider(
width: 10, color: Theme.of(context).colorScheme.primary,
), thickness: 1,
Expanded(child: TopUpButton()), width: 10,
], ),
Expanded(
child: TopUpButton(),
),
],
); );
} }
} }

View File

@@ -4,8 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/source_type.dart';
import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/payment/type.dart';
import 'package:pweb/app/router/payout_routes.dart'; import 'package:pweb/app/router/payout_routes.dart';
@@ -19,27 +18,24 @@ class SendPayoutButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
final source = context.watch<PaymentSourceController>();
final sourceType = source.selectedType;
final paymentType = switch (sourceType) {
PaymentSourceType.wallet => PaymentType.wallet,
PaymentSourceType.ledger => PaymentType.ledger,
_ => null,
};
return ElevatedButton( return ElevatedButton(
style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0), style: ElevatedButton.styleFrom(
onPressed: paymentType == null shadowColor: null,
? null elevation: 0,
: () { ),
context.pushNamed( onPressed: () {
PayoutRoutes.payment, final wallets = context.read<WalletsController>();
queryParameters: PayoutRoutes.buildQueryParameters( final wallet = wallets.selectedWallet;
paymentType: paymentType,
), if (wallet != null) {
); context.pushNamed(
}, PayoutRoutes.payment,
queryParameters: PayoutRoutes.buildQueryParameters(
paymentType: PaymentType.wallet,
),
);
}
},
child: Text(loc.payoutNavSendPayout), child: Text(loc.payoutNavSendPayout),
); );
} }

View File

@@ -1,55 +1,35 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/source_type.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/app/router/payout_routes.dart'; import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
class TopUpButton extends StatelessWidget { class TopUpButton extends StatelessWidget{
const TopUpButton({super.key}); const TopUpButton({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
final source = context.watch<PaymentSourceController>();
final selectedType = source.selectedType;
final selectedLedger = source.selectedLedgerAccount;
final canTopUp =
selectedType == PaymentSourceType.wallet ||
(selectedType == PaymentSourceType.ledger && selectedLedger != null);
return ElevatedButton( return ElevatedButton(
style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0), style: ElevatedButton.styleFrom(
onPressed: !canTopUp shadowColor: null,
? null elevation: 0,
: () { ),
if (selectedType == PaymentSourceType.wallet) { onPressed: () {
context.pushToWalletTopUp(); final wallet = context.read<WalletsController>().selectedWallet;
return; if (wallet == null) {
} ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(loc.noWalletSelected)),
if (selectedType == PaymentSourceType.ledger && );
selectedLedger != null) { return;
context.read<RecipientsProvider>().setCurrentObject(null); }
context.pushNamed( context.pushToWalletTopUp();
PayoutRoutes.payment, },
queryParameters: PayoutRoutes.buildQueryParameters(
paymentType: PaymentType.ledger,
destinationLedgerAccountRef:
selectedLedger.ledgerAccountRef,
),
);
}
},
child: Text(loc.topUpBalance), child: Text(loc.topUpBalance),
); );
} }

View File

@@ -1,30 +1,55 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
import 'package:pweb/widgets/refresh_balance/wallet.dart';
import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/section.dart';
import 'package:pweb/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart';
class WalletEditFields extends StatelessWidget { class WalletEditFields extends StatelessWidget {
const WalletEditFields({super.key}); const WalletEditFields({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<PaymentSourceController>( return Consumer<WalletsController>(
builder: (context, sourceController, _) { builder: (context, controller, _) {
final wallet = sourceController.selectedWallet; final wallet = controller.selectedWallet;
if (wallet != null) {
return WalletSection(wallet: wallet); if (wallet == null) {
return SizedBox.shrink();
} }
final ledger = sourceController.selectedLedgerAccount; return Column(
if (ledger != null) { crossAxisAlignment: CrossAxisAlignment.start,
return LedgerSection(ledger: ledger); children: [
} Row(
children: [
return const SizedBox.shrink(); Expanded(
child: BalanceAmount(
wallet: wallet,
onToggleMask: () => controller.toggleBalanceMask(wallet.id),
),
),
WalletBalanceRefreshButton(walletRef: wallet.id),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(wallet.walletUserID, style: Theme.of(context).textTheme.bodyLarge),
IconButton(
icon: Icon(Icons.copy),
iconSize: 18,
onPressed: () => Clipboard.setData(ClipboardData(text: wallet.walletUserID)),
),
],
),
],
);
}, },
); );
} }

View File

@@ -1,44 +0,0 @@
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
class LedgerBalanceFormatter {
const LedgerBalanceFormatter._();
static String format(LedgerAccount account) {
final money = account.balance?.balance;
if (money == null) return '--';
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
if (amount.isNaN) {
return '${money.amount} ${money.currency}';
}
try {
final currency = currencyStringToCode(money.currency);
final symbol = currencyCodeToSymbol(currency);
if (symbol.trim().isEmpty) {
return '${amountToString(amount)} ${money.currency}';
}
return '${amountToString(amount)} $symbol';
} catch (_) {
return '${amountToString(amount)} ${money.currency}';
}
}
static String formatMasked(LedgerAccount account) {
final currency = account.currency.trim();
if (currency.isEmpty) return '••••';
try {
final symbol = currencyCodeToSymbol(currencyStringToCode(currency));
if (symbol.trim().isEmpty) {
return '•••• $currency';
}
return '•••• $symbol';
} catch (_) {
return '•••• $currency';
}
}
}

View File

@@ -1,39 +0,0 @@
import 'package:flutter/material.dart';
class LedgerBalanceRow extends StatelessWidget {
final String balance;
final bool isMasked;
final VoidCallback onToggleMask;
const LedgerBalanceRow({
super.key,
required this.balance,
required this.isMasked,
required this.onToggleMask,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: Text(
balance,
style: Theme.of(
context,
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: onToggleMask,
child: Icon(
isMasked ? Icons.visibility_off : Icons.visibility,
size: 22,
),
),
],
);
}
}

View File

@@ -1,67 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/ledger_accounts.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart';
import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart';
import 'package:pweb/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart';
import 'package:pweb/widgets/refresh_balance/ledger.dart';
class LedgerSection extends StatelessWidget {
final LedgerAccount ledger;
const LedgerSection({super.key, required this.ledger});
@override
Widget build(BuildContext context) {
return Consumer<LedgerBalanceMaskController>(
builder: (context, balanceMask, _) {
final isMasked = balanceMask.isBalanceMasked(ledger.ledgerAccountRef);
final accountCode = ledger.accountCode.trim();
final hasAccountCode = accountCode.isNotEmpty;
final balance = isMasked
? LedgerBalanceFormatter.formatMasked(ledger)
: LedgerBalanceFormatter.format(ledger);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: LedgerBalanceRow(
balance: balance,
isMasked: isMasked,
onToggleMask: () {
balanceMask.toggleBalanceMask(ledger.ledgerAccountRef);
},
),
),
LedgerBalanceRefreshButton(
ledgerAccountRef: ledger.ledgerAccountRef,
),
],
),
const SizedBox(height: 8),
CopyableValueRow(
value: hasAccountCode ? accountCode : '-',
canCopy: hasAccountCode,
onCopy: hasAccountCode
? () {
Clipboard.setData(ClipboardData(text: accountCode));
}
: null,
overflow: TextOverflow.ellipsis,
wrapValueWithFlexible: true,
),
],
);
},
);
}
}

View File

@@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
class CopyableValueRow extends StatelessWidget {
final String value;
final bool canCopy;
final VoidCallback? onCopy;
final TextOverflow overflow;
final bool wrapValueWithFlexible;
const CopyableValueRow({
super.key,
required this.value,
required this.canCopy,
required this.onCopy,
this.overflow = TextOverflow.visible,
this.wrapValueWithFlexible = false,
});
@override
Widget build(BuildContext context) {
final valueText = Text(
value,
style: Theme.of(context).textTheme.bodyLarge,
overflow: overflow,
);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (wrapValueWithFlexible) Flexible(child: valueText) else valueText,
IconButton(
icon: const Icon(Icons.copy),
iconSize: 18,
onPressed: canCopy ? onCopy : null,
),
],
);
}
}

View File

@@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
import 'package:pweb/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart';
import 'package:pweb/widgets/refresh_balance/wallet.dart';
class WalletSection extends StatelessWidget {
final Wallet wallet;
const WalletSection({super.key, required this.wallet});
@override
Widget build(BuildContext context) {
final depositAddress = wallet.depositAddress?.trim();
final hasDepositAddress =
depositAddress != null && depositAddress.isNotEmpty;
final copyAddress = hasDepositAddress ? depositAddress : '';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: BalanceAmount(
wallet: wallet,
onToggleMask: () {
context.read<WalletsController>().toggleBalanceMask(
wallet.id,
);
},
),
),
WalletBalanceRefreshButton(walletRef: wallet.id),
],
),
const SizedBox(height: 8),
CopyableValueRow(
value: hasDepositAddress ? depositAddress : '-',
canCopy: hasDepositAddress,
onCopy: hasDepositAddress
? () {
Clipboard.setData(ClipboardData(text: copyAddress));
}
: null,
),
],
);
}
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -12,20 +12,15 @@ class WalletEditHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = context.watch<PaymentSourceController>(); final controller = context.watch<WalletsController>();
final wallet = controller.selectedWallet; final wallet = controller.selectedWallet;
final ledger = controller.selectedLedgerAccount;
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
if (wallet == null && ledger == null) { if (wallet == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final theme = Theme.of(context); final theme = Theme.of(context);
final title = wallet != null
? loc.paymentTypeCryptoWallet
: loc.paymentTypeLedger;
final subtitle = wallet?.tokenSymbol;
return Row( return Row(
spacing: 8, spacing: 8,
@@ -37,14 +32,14 @@ class WalletEditHeader extends StatelessWidget {
spacing: 4, spacing: 4,
children: [ children: [
Text( Text(
title, loc.paymentTypeCryptoWallet,
style: theme.textTheme.headlineMedium?.copyWith( style: theme.textTheme.headlineMedium!.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
if (subtitle != null && subtitle.trim().isNotEmpty) if (wallet.tokenSymbol != null)
Text( Text(
subtitle, wallet.tokenSymbol!,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),

View File

@@ -2,16 +2,17 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pweb/pages/payout_page/wallet/edit/buttons/buttons.dart'; import 'package:pweb/pages/payout_page/wallet/edit/buttons/buttons.dart';
import 'package:pweb/pages/payout_page/wallet/edit/fields.dart'; import 'package:pweb/pages/payout_page/wallet/edit/fields.dart';
import 'package:pweb/pages/payout_page/wallet/edit/header.dart'; import 'package:pweb/pages/payout_page/wallet/edit/header.dart';
import 'package:pweb/pages/payout_page/wallet/history/history.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';
//TODO make this page more generic and reusable
class WalletEditPage extends StatelessWidget { class WalletEditPage extends StatelessWidget {
final VoidCallback onBack; final VoidCallback onBack;
@@ -22,11 +23,11 @@ class WalletEditPage extends StatelessWidget {
final dimensions = AppDimensions(); final dimensions = AppDimensions();
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
return Consumer<PaymentSourceController>( return Consumer<WalletsController>(
builder: (context, controller, child) { builder: (context, controller, child) {
final sourceType = controller.selectedType; final wallet = controller.selectedWallet;
if (sourceType == null) { if (wallet == null) {
return Center(child: Text(loc.noWalletSelected)); return Center(child: Text(loc.noWalletSelected));
} }
@@ -35,15 +36,11 @@ class WalletEditPage extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth),
maxWidth: dimensions.maxContentWidth,
),
child: Material( child: Material(
elevation: dimensions.elevationSmall, elevation: dimensions.elevationSmall,
color: Theme.of(context).colorScheme.onSecondary, color: Theme.of(context).colorScheme.onSecondary,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium),
dimensions.borderRadiusMedium,
),
child: Padding( child: Padding(
padding: EdgeInsets.all(dimensions.paddingLarge), padding: EdgeInsets.all(dimensions.paddingLarge),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -58,12 +55,19 @@ class WalletEditPage extends StatelessWidget {
WalletEditFields(), WalletEditFields(),
const SizedBox(height: 24), const SizedBox(height: 24),
ButtonsWalletWidget(), ButtonsWalletWidget(),
const SizedBox(height: 24),
], ],
), ),
), ),
), ),
), ),
), ),
const SizedBox(height: 24),
Expanded(
child: SingleChildScrollView(
child: WalletHistory(wallet: wallet),
),
),
], ],
), ),
); );

View File

@@ -2,87 +2,118 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/models/payment/source_type.dart'; import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/provider/payment/payments.dart';
import 'package:pweb/controllers/operations/report_operations.dart'; import 'package:pweb/pages/payout_page/wallet/history/filters.dart';
import 'package:pweb/models/state/load_more_state.dart'; import 'package:pweb/pages/payout_page/wallet/history/table.dart';
import 'package:pweb/pages/report/cards/list.dart'; import 'package:pweb/controllers/operations/wallet_transactions.dart';
import 'package:pweb/pages/report/operations/actions.dart'; import 'package:pweb/providers/wallet_transactions.dart';
import 'package:pweb/pages/report/operations/states/error.dart';
import 'package:pweb/pages/report/operations/states/loading.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletHistory extends StatelessWidget { class WalletHistory extends StatefulWidget {
final String sourceRef; final Wallet wallet;
final PaymentSourceType sourceType;
final List<String> sourceRefs;
const WalletHistory({ const WalletHistory({super.key, required this.wallet});
super.key,
required this.sourceRef,
required this.sourceType,
required this.sourceRefs,
});
@override @override
Widget build(BuildContext context) { State<WalletHistory> createState() => _WalletHistoryState();
return ChangeNotifierProxyProvider<
PaymentsProvider,
ReportOperationsController
>(
create: (_) => ReportOperationsController(),
update: (_, payments, controller) => controller!
..update(
payments,
sourceType: sourceType,
sourceRef: sourceRef,
sourceRefs: sourceRefs,
),
child: const _WalletHistoryContent(),
);
}
} }
class _WalletHistoryContent extends StatelessWidget { class _WalletHistoryState extends State<WalletHistory> {
const _WalletHistoryContent(); @override
void initState() {
super.initState();
_load();
}
@override
void didUpdateWidget(covariant WalletHistory oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.wallet.id != widget.wallet.id) {
_load();
}
}
void _load() {
WidgetsBinding.instance.addPostFrameCallback((_) {
context
.read<WalletTransactionsProvider>()
.load(walletId: widget.wallet.id);
});
}
Future<void> _pickRange() async {
final provider = context.read<WalletTransactionsController>();
final now = DateTime.now();
final initial = provider.dateRange ??
DateTimeRange(
start: now.subtract(const Duration(days: 30)),
end: now,
);
final picked = await showDateRangePicker(
context: context,
firstDate: now.subtract(const Duration(days: 365)),
lastDate: now.add(const Duration(days: 1)),
initialDateRange: initial,
);
if (picked != null) {
provider.setDateRange(picked);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
return Consumer<ReportOperationsController>( return Consumer<WalletTransactionsController>(
builder: (context, controller, child) { builder: (context, provider, child) {
if (controller.isLoading) { if (provider.isLoading) {
return const OperationHistoryLoading(); return const Padding(
} padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
if (controller.error != null) {
final message =
controller.error?.toString() ?? loc.noErrorInformation;
return OperationHistoryError(
message: loc.notificationError(message),
retryLabel: loc.retry,
onRetry: controller.refresh,
); );
} }
final hasLoadMore = controller.loadMoreState != LoadMoreState.hidden; if (provider.error != null) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
OperationsCardsList( Text(
operations: controller.filteredOperations, loc.failedToLoadHistory,
onTap: (operation) => openPaymentDetails(context, operation), style: theme.textTheme.titleMedium!
loadMoreState: controller.loadMoreState, .copyWith(color: theme.colorScheme.error),
onLoadMore: hasLoadMore ? controller.loadMore : null, ),
), const SizedBox(height: 8),
], Text(loc.notificationError(provider.error ?? loc.noErrorInformation)),
), const SizedBox(height: 8),
OutlinedButton(
onPressed: _load,
child: Text(loc.retry),
),
],
),
);
}
final transactions = provider.filteredTransactions;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
WalletHistoryFilters(
provider: provider,
onPickRange: _pickRange,
),
const SizedBox(height: 12),
WalletTransactionsTable(transactions: transactions),
],
); );
}, },
); );

View File

@@ -13,6 +13,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentDetailsContent extends StatelessWidget { class PaymentDetailsContent extends StatelessWidget {
final Payment payment; final Payment payment;
final VoidCallback onBack; final VoidCallback onBack;
final VoidCallback? onDownloadAct;
final bool Function(PaymentExecutionOperation operation)? final bool Function(PaymentExecutionOperation operation)?
canDownloadOperationDocument; canDownloadOperationDocument;
final ValueChanged<PaymentExecutionOperation>? onDownloadOperationDocument; final ValueChanged<PaymentExecutionOperation>? onDownloadOperationDocument;
@@ -21,6 +22,7 @@ class PaymentDetailsContent extends StatelessWidget {
super.key, super.key,
required this.payment, required this.payment,
required this.onBack, required this.onBack,
this.onDownloadAct,
this.canDownloadOperationDocument, this.canDownloadOperationDocument,
this.onDownloadOperationDocument, this.onDownloadOperationDocument,
}); });
@@ -35,7 +37,7 @@ class PaymentDetailsContent extends StatelessWidget {
children: [ children: [
PaymentDetailsHeader(title: loc.paymentInfo, onBack: onBack), PaymentDetailsHeader(title: loc.paymentInfo, onBack: onBack),
const SizedBox(height: 16), const SizedBox(height: 16),
PaymentSummaryCard(payment: payment), PaymentSummaryCard(payment: payment, onDownloadAct: onDownloadAct),
const SizedBox(height: 16), const SizedBox(height: 16),
PaymentDetailsSections( PaymentDetailsSections(
payment: payment, payment: payment,

View File

@@ -64,6 +64,17 @@ class _PaymentDetailsView extends StatelessWidget {
return PaymentDetailsContent( return PaymentDetailsContent(
payment: payment, payment: payment,
onBack: () => _handleBack(context), onBack: () => _handleBack(context),
onDownloadAct: controller.canDownload
? () {
final request = controller.primaryOperationDocumentRequest;
if (request == null) return;
downloadPaymentAct(
context,
gatewayService: request.gatewayService,
operationRef: request.operationRef,
);
}
: null,
canDownloadOperationDocument: canDownloadOperationDocument:
controller.canDownloadOperationDocument, controller.canDownloadOperationDocument,
onDownloadOperationDocument: (operation) { onDownloadOperationDocument: (operation) {

View File

@@ -44,12 +44,12 @@ class PaymentOperationsSection extends StatelessWidget {
); );
if (i < operations.length - 1) { if (i < operations.length - 1) {
children.addAll([ children.addAll([
const SizedBox(height: 10), const SizedBox(height: 8),
Divider( Divider(
height: 1, height: 1,
color: Theme.of(context).dividerColor.withAlpha(20), color: Theme.of(context).dividerColor.withAlpha(20),
), ),
const SizedBox(height: 10), const SizedBox(height: 8),
]); ]);
} }
} }

View File

@@ -14,11 +14,15 @@ import 'package:pweb/utils/clipboard.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentSummaryCard extends StatelessWidget { class PaymentSummaryCard extends StatelessWidget {
final Payment payment; final Payment payment;
final VoidCallback? onDownloadAct;
const PaymentSummaryCard({super.key, required this.payment}); const PaymentSummaryCard({
super.key,
required this.payment,
this.onDownloadAct,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -89,6 +93,14 @@ class PaymentSummaryCard extends StatelessWidget {
text: feeText, text: feeText,
muted: true, muted: true,
), ),
if (onDownloadAct != null) ...[
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: onDownloadAct,
icon: const Icon(Icons.download),
label: Text(loc.downloadAct),
),
],
if (showPaymentId) ...[ if (showPaymentId) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
Divider(color: theme.dividerColor.withAlpha(35), height: 1), Divider(color: theme.dividerColor.withAlpha(35), height: 1),

View File

@@ -13,9 +13,8 @@ import 'package:pshared/utils/payment/quote_helpers.dart';
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart'; import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
import 'package:pweb/models/payment/multiple_payouts/state.dart'; import 'package:pweb/models/payment/multiple_payouts/state.dart';
import 'package:pweb/utils/payment/multiple/csv_parser.dart'; import 'package:pweb/utils/payment/multiple_csv_parser.dart';
import 'package:pweb/utils/payment/multiple/intent_builder.dart'; import 'package:pweb/utils/payment/multiple_intent_builder.dart';
class MultiplePayoutsProvider extends ChangeNotifier { class MultiplePayoutsProvider extends ChangeNotifier {
final MultipleCsvParser _csvParser; final MultipleCsvParser _csvParser;

View File

@@ -13,7 +13,6 @@ class WalletTransactionsProvider extends ChangeNotifier {
bool _isLoading = false; bool _isLoading = false;
String? _error; String? _error;
String? _walletId; String? _walletId;
int _loadSeq = 0;
List<WalletTransaction> get transactions => List.unmodifiable(_transactions); List<WalletTransaction> get transactions => List.unmodifiable(_transactions);
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
@@ -21,28 +20,18 @@ class WalletTransactionsProvider extends ChangeNotifier {
String? get walletId => _walletId; String? get walletId => _walletId;
Future<void> load({String? walletId}) async { Future<void> load({String? walletId}) async {
final targetWalletId = walletId ?? _walletId;
final requestSeq = ++_loadSeq;
_walletId = targetWalletId;
_isLoading = true; _isLoading = true;
_error = null; _error = null;
notifyListeners(); notifyListeners();
try { try {
final fetched = await _service.fetchHistory(walletId: targetWalletId); _walletId = walletId ?? _walletId;
if (requestSeq != _loadSeq) return; _transactions = await _service.fetchHistory(walletId: _walletId);
_transactions = targetWalletId == null
? fetched
: fetched.where((tx) => tx.walletId == targetWalletId).toList();
} catch (e) { } catch (e) {
if (requestSeq != _loadSeq) return;
_error = e.toString(); _error = e.toString();
} finally { } finally {
if (requestSeq == _loadSeq) { _isLoading = false;
_isLoading = false; notifyListeners();
notifyListeners();
}
} }
} }
} }

View File

@@ -1,13 +1,12 @@
import 'package:pshared/models/payment/operation_document.dart';
import 'package:pshared/models/payment/operation.dart'; import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/state.dart'; import 'package:pshared/models/payment/state.dart';
import 'package:pshared/models/payment/status.dart'; import 'package:pshared/models/payment/status.dart';
import 'package:pshared/utils/money.dart'; import 'package:pshared/utils/money.dart';
import 'package:pweb/models/report/operation/document.dart';
import 'package:pweb/utils/report/operations/document_rule.dart'; import 'package:pweb/utils/report/operations/document_rule.dart';
OperationItem mapPaymentToOperation(Payment payment) { OperationItem mapPaymentToOperation(Payment payment) {
final debit = payment.lastQuote?.amounts?.sourceDebitTotal; final debit = payment.lastQuote?.amounts?.sourceDebitTotal;
final settlement = payment.lastQuote?.amounts?.destinationSettlement; final settlement = payment.lastQuote?.amounts?.destinationSettlement;
@@ -56,7 +55,7 @@ OperationItem mapPaymentToOperation(Payment payment) {
); );
} }
OperationDocumentRef? _resolveOperationDocument(Payment payment) { OperationDocumentInfo? _resolveOperationDocument(Payment payment) {
for (final operation in payment.operations) { for (final operation in payment.operations) {
final operationRef = operation.operationRef; final operationRef = operation.operationRef;
final gatewayService = operation.gateway; final gatewayService = operation.gateway;
@@ -65,7 +64,7 @@ OperationDocumentRef? _resolveOperationDocument(Payment payment) {
if (!isOperationDocumentEligible(operation.code)) continue; if (!isOperationDocumentEligible(operation.code)) continue;
return OperationDocumentRef( return OperationDocumentInfo(
operationRef: operationRef, operationRef: operationRef,
gatewayService: gatewayService, gatewayService: gatewayService,
); );

View File

@@ -1,112 +0,0 @@
import 'package:pshared/models/payment/intent.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/ledger.dart';
import 'package:pshared/models/payment/methods/managed_wallet.dart';
import 'package:pshared/models/payment/methods/wallet.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/source_type.dart';
bool paymentMatchesSource(
Payment payment, {
required PaymentSourceType sourceType,
required String sourceRef,
}) {
final normalizedSourceRef = _normalize(sourceRef);
if (normalizedSourceRef == null) return false;
final paymentSourceRef = _paymentSourceRef(payment, sourceType);
return paymentSourceRef != null && paymentSourceRef == normalizedSourceRef;
}
String? _paymentSourceRef(Payment payment, PaymentSourceType sourceType) {
final fromIntent = _sourceRefFromIntent(payment.intent, sourceType);
if (fromIntent != null) return fromIntent;
return _sourceRefFromMetadata(payment.metadata, sourceType);
}
String? _sourceRefFromIntent(
PaymentIntent? intent,
PaymentSourceType sourceType,
) {
final source = intent?.source;
if (source == null) return null;
final fromIntentAttributes = _sourceRefFromMetadata(
intent?.attributes,
sourceType,
);
if (fromIntentAttributes != null) return fromIntentAttributes;
switch (sourceType) {
case PaymentSourceType.wallet:
return _walletSourceRef(source);
case PaymentSourceType.ledger:
return _ledgerSourceRef(source);
}
}
String? _walletSourceRef(PaymentMethodData source) {
if (source is ManagedWalletPaymentMethod) {
return _normalize(source.managedWalletRef) ??
_sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet);
}
if (source is WalletPaymentMethod) {
return _normalize(source.walletId) ??
_sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet);
}
return null;
}
String? _ledgerSourceRef(PaymentMethodData source) {
if (source is LedgerPaymentMethod) {
return _normalize(source.ledgerAccountRef) ??
_sourceRefFromMetadata(source.metadata, PaymentSourceType.ledger);
}
return null;
}
String? _sourceRefFromMetadata(
Map<String, String>? metadata,
PaymentSourceType sourceType,
) {
if (metadata == null || metadata.isEmpty) return null;
final keys = switch (sourceType) {
PaymentSourceType.wallet => const <String>[
'source_wallet_ref',
'managed_wallet_ref',
'wallet_ref',
'wallet_id',
'source_wallet_id',
'source_wallet_user_id',
'wallet_user_id',
'wallet_user_ref',
'wallet_number',
'source_wallet_number',
'source_managed_wallet_ref',
'source_ref',
],
PaymentSourceType.ledger => const <String>[
'source_ledger_account_ref',
'ledger_account_ref',
'source_account_code',
'ledger_account_code',
'account_code',
'source_ref',
],
};
for (final key in keys) {
final value = _normalize(metadata[key]);
if (value != null) return value;
}
return null;
}
String? _normalize(String? value) {
final normalized = value?.trim();
if (normalized == null || normalized.isEmpty) return null;
return normalized;
}