Compare commits
36 Commits
659b90b6a5
...
SEND003
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfbf36bf04 | ||
|
|
b16c295094 | ||
|
|
336687eccf | ||
| f478219990 | |||
|
|
bf39b1d401 | ||
| f7bf3138ac | |||
|
|
7cb747f9a9 | ||
| f2658aea44 | |||
|
|
5e49ee3244 | ||
| 1073be187f | |||
|
|
e854963fa6 | ||
| e5f283432b | |||
|
|
d62a3413b2 | ||
| f720ba9bdf | |||
|
|
98f254e34b | ||
|
|
980bb96c74 | ||
| 4bb18f0210 | |||
|
|
574b40fe9f | ||
| a3a807e625 | |||
|
|
3b047af7ca | ||
| 36cc46577c | |||
|
|
e1da16448b | ||
| fed6f39de6 | |||
|
|
85fb567ed9 | ||
|
|
fd07c10cba | ||
|
|
c44edc85fa | ||
| 57a48fe2a3 | |||
|
|
2b2a8afc2f | ||
| d431317a50 | |||
|
|
b4c696f1ef | ||
| 4d03a6ead8 | |||
|
|
2fe5151650 | ||
|
|
2754a7aa13 | ||
|
|
f71cc76f64 | ||
| 082d782a80 | |||
|
|
18f8f3c476 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,4 +8,5 @@ devtools_options.yaml
|
|||||||
untranslated.txt
|
untranslated.txt
|
||||||
generate_protos.sh
|
generate_protos.sh
|
||||||
update_dep.sh
|
update_dep.sh
|
||||||
.vscode/
|
.vscode/
|
||||||
|
GeneratedPluginRegistrant.swift
|
||||||
@@ -55,3 +55,6 @@ key_management:
|
|||||||
namespace: ""
|
namespace: ""
|
||||||
mount_path: kv
|
mount_path: kv
|
||||||
key_prefix: gateway/chain/wallets
|
key_prefix: gateway/chain/wallets
|
||||||
|
|
||||||
|
cache:
|
||||||
|
wallet_balance_ttl_seconds: 120
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ require (
|
|||||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||||
github.com/holiman/uint256 v1.3.2 // indirect
|
github.com/holiman/uint256 v1.3.2 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
@@ -86,5 +86,5 @@ require (
|
|||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
|||||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -362,8 +362,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ type Imp struct {
|
|||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
*grpcapp.Config `yaml:",inline"`
|
*grpcapp.Config `yaml:",inline"`
|
||||||
Chains []chainConfig `yaml:"chains"`
|
Chains []chainConfig `yaml:"chains"`
|
||||||
ServiceWallet serviceWalletConfig `yaml:"service_wallet"`
|
ServiceWallet serviceWalletConfig `yaml:"service_wallet"`
|
||||||
KeyManagement keymanager.Config `yaml:"key_management"`
|
KeyManagement keymanager.Config `yaml:"key_management"`
|
||||||
|
Settings gatewayservice.CacheSettings `yaml:"cache"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type chainConfig struct {
|
type chainConfig struct {
|
||||||
@@ -111,11 +112,12 @@ func (i *Imp) Start() error {
|
|||||||
gatewayservice.WithServiceWallet(walletConfig),
|
gatewayservice.WithServiceWallet(walletConfig),
|
||||||
gatewayservice.WithKeyManager(keyManager),
|
gatewayservice.WithKeyManager(keyManager),
|
||||||
gatewayservice.WithTransferExecutor(executor),
|
gatewayservice.WithTransferExecutor(executor),
|
||||||
|
gatewayservice.WithSettings(cfg.Settings),
|
||||||
}
|
}
|
||||||
return gatewayservice.NewService(logger, repo, producer, opts...), nil
|
return gatewayservice.NewService(logger, repo, producer, opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := grpcapp.NewApp(i.logger, "chain_gateway", cfg.Config, i.debug, repoFactory, serviceFactory)
|
app, err := grpcapp.NewApp(i.logger, "chain", cfg.Config, i.debug, repoFactory, serviceFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
@@ -14,6 +17,8 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const fallbackBalanceCacheTTL = 2 * time.Minute
|
||||||
|
|
||||||
type getWalletBalanceCommand struct {
|
type getWalletBalanceCommand struct {
|
||||||
deps Deps
|
deps Deps
|
||||||
}
|
}
|
||||||
@@ -48,30 +53,88 @@ func (c *getWalletBalanceCommand) Execute(ctx context.Context, req *chainv1.GetW
|
|||||||
|
|
||||||
balance, chainErr := onChainWalletBalance(ctx, c.deps, wallet)
|
balance, chainErr := onChainWalletBalance(ctx, c.deps, wallet)
|
||||||
if chainErr != nil {
|
if chainErr != nil {
|
||||||
c.deps.Logger.Warn("on-chain balance fetch failed, falling back to stored balance", zap.Error(chainErr), zap.String("wallet_ref", walletRef))
|
c.deps.Logger.Warn("on-chain balance fetch failed, attempting cached balance", zap.Error(chainErr), zap.String("wallet_ref", walletRef))
|
||||||
stored, err := c.deps.Storage.Wallets().GetBalance(ctx, walletRef)
|
stored, err := c.deps.Storage.Wallets().GetBalance(ctx, walletRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
c.deps.Logger.Warn("stored balance not found", zap.String("wallet_ref", walletRef))
|
c.deps.Logger.Warn("cached balance not found", zap.String("wallet_ref", walletRef))
|
||||||
return gsresponse.NotFound[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, chainErr)
|
||||||
}
|
}
|
||||||
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
}
|
}
|
||||||
|
if c.isCachedBalanceStale(stored) {
|
||||||
|
c.deps.Logger.Warn("cached balance is stale",
|
||||||
|
zap.String("wallet_ref", walletRef),
|
||||||
|
zap.Time("calculated_at", stored.CalculatedAt),
|
||||||
|
zap.Duration("ttl", c.cacheTTL()),
|
||||||
|
)
|
||||||
|
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, chainErr)
|
||||||
|
}
|
||||||
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(stored)})
|
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(stored)})
|
||||||
}
|
}
|
||||||
|
|
||||||
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: onChainBalanceToProto(balance)})
|
calculatedAt := c.now()
|
||||||
|
c.persistCachedBalance(ctx, walletRef, balance, calculatedAt)
|
||||||
|
|
||||||
|
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{
|
||||||
|
Balance: onChainBalanceToProto(balance, calculatedAt),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func onChainBalanceToProto(balance *moneyv1.Money) *chainv1.WalletBalance {
|
func onChainBalanceToProto(balance *moneyv1.Money, calculatedAt time.Time) *chainv1.WalletBalance {
|
||||||
if balance == nil {
|
if balance == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
zero := &moneyv1.Money{Currency: balance.Currency, Amount: "0"}
|
zero := zeroMoney(balance.Currency)
|
||||||
return &chainv1.WalletBalance{
|
return &chainv1.WalletBalance{
|
||||||
Available: balance,
|
Available: balance,
|
||||||
PendingInbound: zero,
|
PendingInbound: zero,
|
||||||
PendingOutbound: zero,
|
PendingOutbound: zero,
|
||||||
CalculatedAt: timestamppb.Now(),
|
CalculatedAt: timestamppb.New(calculatedAt.UTC()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *getWalletBalanceCommand) persistCachedBalance(ctx context.Context, walletRef string, available *moneyv1.Money, calculatedAt time.Time) {
|
||||||
|
if available == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record := &model.WalletBalance{
|
||||||
|
WalletRef: walletRef,
|
||||||
|
Available: shared.CloneMoney(available),
|
||||||
|
PendingInbound: zeroMoney(available.Currency),
|
||||||
|
PendingOutbound: zeroMoney(available.Currency),
|
||||||
|
CalculatedAt: calculatedAt,
|
||||||
|
}
|
||||||
|
if err := c.deps.Storage.Wallets().SaveBalance(ctx, record); err != nil {
|
||||||
|
c.deps.Logger.Warn("failed to cache wallet balance", zap.String("wallet_ref", walletRef), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getWalletBalanceCommand) isCachedBalanceStale(balance *model.WalletBalance) bool {
|
||||||
|
if balance == nil || balance.CalculatedAt.IsZero() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return c.now().After(balance.CalculatedAt.Add(c.cacheTTL()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getWalletBalanceCommand) cacheTTL() time.Duration {
|
||||||
|
if c.deps.BalanceCacheTTL > 0 {
|
||||||
|
return c.deps.BalanceCacheTTL
|
||||||
|
}
|
||||||
|
// Fallback to sane default if not configured.
|
||||||
|
return fallbackBalanceCacheTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getWalletBalanceCommand) now() time.Time {
|
||||||
|
if c.deps.Clock != nil {
|
||||||
|
return c.deps.Clock.Now().UTC()
|
||||||
|
}
|
||||||
|
return time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
func zeroMoney(currency string) *moneyv1.Money {
|
||||||
|
if strings.TrimSpace(currency) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &moneyv1.Money{Currency: currency, Amount: "0"}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package wallet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/gateway/chain/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,6 +16,8 @@ type Deps struct {
|
|||||||
Networks map[string]shared.Network
|
Networks map[string]shared.Network
|
||||||
KeyManager keymanager.Manager
|
KeyManager keymanager.Manager
|
||||||
Storage storage.Repository
|
Storage storage.Repository
|
||||||
|
Clock clockpkg.Clock
|
||||||
|
BalanceCacheTTL time.Duration
|
||||||
EnsureRepository func(context.Context) error
|
EnsureRepository func(context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,3 +67,10 @@ func WithClock(clk clockpkg.Clock) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSettings applies gateway settings.
|
||||||
|
func WithSettings(settings CacheSettings) Option {
|
||||||
|
return func(s *Service) {
|
||||||
|
s.settings = settings.withDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ type Service struct {
|
|||||||
producer msg.Producer
|
producer msg.Producer
|
||||||
clock clockpkg.Clock
|
clock clockpkg.Clock
|
||||||
|
|
||||||
|
settings CacheSettings
|
||||||
|
|
||||||
networks map[string]shared.Network
|
networks map[string]shared.Network
|
||||||
serviceWallet shared.ServiceWallet
|
serviceWallet shared.ServiceWallet
|
||||||
keyManager keymanager.Manager
|
keyManager keymanager.Manager
|
||||||
@@ -52,6 +54,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
|||||||
storage: repo,
|
storage: repo,
|
||||||
producer: producer,
|
producer: producer,
|
||||||
clock: clockpkg.System{},
|
clock: clockpkg.System{},
|
||||||
|
settings: defaultSettings(),
|
||||||
networks: map[string]shared.Network{},
|
networks: map[string]shared.Network{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +72,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
|||||||
if svc.networks == nil {
|
if svc.networks == nil {
|
||||||
svc.networks = map[string]shared.Network{}
|
svc.networks = map[string]shared.Network{}
|
||||||
}
|
}
|
||||||
|
svc.settings = svc.settings.withDefaults()
|
||||||
|
|
||||||
svc.commands = commands.NewRegistry(commands.RegistryDeps{
|
svc.commands = commands.NewRegistry(commands.RegistryDeps{
|
||||||
Wallet: commandsWalletDeps(svc),
|
Wallet: commandsWalletDeps(svc),
|
||||||
@@ -130,6 +134,8 @@ func commandsWalletDeps(s *Service) wallet.Deps {
|
|||||||
Networks: s.networks,
|
Networks: s.networks,
|
||||||
KeyManager: s.keyManager,
|
KeyManager: s.keyManager,
|
||||||
Storage: s.storage,
|
Storage: s.storage,
|
||||||
|
Clock: s.clock,
|
||||||
|
BalanceCacheTTL: s.settings.walletBalanceCacheTTL(),
|
||||||
EnsureRepository: s.ensureRepository,
|
EnsureRepository: s.ensureRepository,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
api/gateway/chain/internal/service/gateway/settings.go
Normal file
30
api/gateway/chain/internal/service/gateway/settings.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const defaultWalletBalanceCacheTTL = 120 * time.Second
|
||||||
|
|
||||||
|
// CacheSettings holds tunable gateway behaviour.
|
||||||
|
type CacheSettings struct {
|
||||||
|
WalletBalanceCacheTTLSeconds int `yaml:"wallet_balance_ttl_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSettings() CacheSettings {
|
||||||
|
return CacheSettings{
|
||||||
|
WalletBalanceCacheTTLSeconds: int(defaultWalletBalanceCacheTTL.Seconds()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s CacheSettings) withDefaults() CacheSettings {
|
||||||
|
if s.WalletBalanceCacheTTLSeconds <= 0 {
|
||||||
|
s.WalletBalanceCacheTTLSeconds = int(defaultWalletBalanceCacheTTL.Seconds())
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s CacheSettings) walletBalanceCacheTTL() time.Duration {
|
||||||
|
if s.WalletBalanceCacheTTLSeconds <= 0 {
|
||||||
|
return defaultWalletBalanceCacheTTL
|
||||||
|
}
|
||||||
|
return time.Duration(s.WalletBalanceCacheTTLSeconds) * time.Second
|
||||||
|
}
|
||||||
@@ -13,5 +13,5 @@ func factory(logger mlogger.Logger, file string, debug bool) (server.Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
smain.RunServer("main", appversion.Create(), factory)
|
smain.RunServer("gateway", appversion.Create(), factory)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ This service now supports Monetix “payout by card”.
|
|||||||
|
|
||||||
## Runtime entry points
|
## Runtime entry points
|
||||||
- gRPC: `MntxGatewayService.CreateCardPayout` and `GetCardPayoutStatus`.
|
- gRPC: `MntxGatewayService.CreateCardPayout` and `GetCardPayoutStatus`.
|
||||||
- Callback HTTP server (default): `:8080/monetix/callback` for Monetix payout status notifications.
|
- Callback HTTP server (default): `:8084/monetix/callback` for Monetix payout status notifications.
|
||||||
- Metrics: Prometheus on `:9404/metrics`.
|
- Metrics: Prometheus on `:9404/metrics`.
|
||||||
|
|
||||||
## Required config/env
|
## Required config/env
|
||||||
@@ -13,7 +13,7 @@ This service now supports Monetix “payout by card”.
|
|||||||
- `MONETIX_PROJECT_ID` – integer project ID
|
- `MONETIX_PROJECT_ID` – integer project ID
|
||||||
- `MONETIX_SECRET_KEY` – signature secret
|
- `MONETIX_SECRET_KEY` – signature secret
|
||||||
- Optional: `allowed_currencies`, `require_customer_address`, `request_timeout_seconds`
|
- Optional: `allowed_currencies`, `require_customer_address`, `request_timeout_seconds`
|
||||||
- Callback server: `MNTX_GATEWAY_HTTP_PORT` (exposed as 8080), `http.callback.path`, optional `allowed_cidrs`
|
- Callback server: `MNTX_GATEWAY_HTTP_PORT` (exposed as 8084), `http.callback.path`, optional `allowed_cidrs`
|
||||||
|
|
||||||
## Outbound request (CreateCardPayout)
|
## Outbound request (CreateCardPayout)
|
||||||
Payload is built per Monetix spec:
|
Payload is built per Monetix spec:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ monetix:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
callback:
|
callback:
|
||||||
address: ":8080"
|
address: ":8084"
|
||||||
path: "/monetix/callback"
|
path: "/monetix/callback"
|
||||||
allowed_cidrs: []
|
allowed_cidrs: []
|
||||||
max_body_bytes: 1048576
|
max_body_bytes: 1048576
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ go 1.25.3
|
|||||||
replace github.com/tech/sendico/pkg => ../../pkg
|
replace github.com/tech/sendico/pkg => ../../pkg
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/shopspring/decimal v1.4.0
|
github.com/shopspring/decimal v1.4.0
|
||||||
@@ -22,7 +23,6 @@ require (
|
|||||||
github.com/casbin/govaluate v1.10.0 // indirect
|
github.com/casbin/govaluate v1.10.0 // indirect
|
||||||
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
|
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.2 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
mntxservice "github.com/tech/sendico/gateway/mntx/internal/service/gateway"
|
mntxservice "github.com/tech/sendico/gateway/mntx/internal/service/gateway"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||||
"github.com/tech/sendico/pkg/api/routers"
|
"github.com/tech/sendico/pkg/api/routers"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
@@ -124,7 +125,7 @@ func (i *Imp) Start() error {
|
|||||||
return svc, nil
|
return svc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := grpcapp.NewApp(i.logger, "mntx_gateway", cfg.Config, i.debug, nil, serviceFactory)
|
app, err := grpcapp.NewApp(i.logger, "monetix", cfg.Config, i.debug, nil, serviceFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -162,13 +163,13 @@ func (i *Imp) loadConfig() (*config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Metrics == nil {
|
if cfg.Metrics == nil {
|
||||||
cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9404"}
|
cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9405"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (mntxservice.MonetixConfig, error) {
|
func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (monetix.Config, error) {
|
||||||
baseURL := strings.TrimSpace(cfg.BaseURL)
|
baseURL := strings.TrimSpace(cfg.BaseURL)
|
||||||
if env := strings.TrimSpace(cfg.BaseURLEnv); env != "" {
|
if env := strings.TrimSpace(cfg.BaseURLEnv); env != "" {
|
||||||
if val := strings.TrimSpace(os.Getenv(env)); val != "" {
|
if val := strings.TrimSpace(os.Getenv(env)); val != "" {
|
||||||
@@ -183,7 +184,7 @@ func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (mntxservice.MonetixConfig
|
|||||||
if id, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
if id, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||||
projectID = id
|
projectID = id
|
||||||
} else {
|
} else {
|
||||||
return mntxservice.MonetixConfig{}, merrors.InvalidArgument("invalid project id in env "+cfg.ProjectIDEnv, "monetix.project_id")
|
return monetix.Config{}, merrors.InvalidArgument("invalid project id in env "+cfg.ProjectIDEnv, "monetix.project_id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +204,7 @@ func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (mntxservice.MonetixConfig
|
|||||||
statusSuccess := strings.TrimSpace(cfg.StatusSuccess)
|
statusSuccess := strings.TrimSpace(cfg.StatusSuccess)
|
||||||
statusProcessing := strings.TrimSpace(cfg.StatusProcessing)
|
statusProcessing := strings.TrimSpace(cfg.StatusProcessing)
|
||||||
|
|
||||||
return mntxservice.MonetixConfig{
|
return monetix.Config{
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
SecretKey: secret,
|
SecretKey: secret,
|
||||||
@@ -225,7 +226,7 @@ type callbackRuntimeConfig struct {
|
|||||||
func (i *Imp) resolveCallbackConfig(cfg callbackConfig) (callbackRuntimeConfig, error) {
|
func (i *Imp) resolveCallbackConfig(cfg callbackConfig) (callbackRuntimeConfig, error) {
|
||||||
addr := strings.TrimSpace(cfg.Address)
|
addr := strings.TrimSpace(cfg.Address)
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
addr = ":8080"
|
addr = ":8084"
|
||||||
}
|
}
|
||||||
path := strings.TrimSpace(cfg.Path)
|
path := strings.TrimSpace(cfg.Path)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -31,7 +30,7 @@ func NewClient(cfg Config, httpClient *http.Client, logger mlogger.Logger) *Clie
|
|||||||
return &Client{
|
return &Client{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
client: client,
|
client: client,
|
||||||
logger: cl.Named("monetix_client"),
|
logger: cl.Named("client"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ func factory(logger mlogger.Logger, file string, debug bool) (server.Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
smain.RunServer("mntx_gateway", appversion.Create(), factory)
|
smain.RunServer("gateway", appversion.Create(), factory)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ require (
|
|||||||
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@@ -51,5 +51,5 @@ require (
|
|||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -214,8 +214,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ require (
|
|||||||
github.com/go-test/deep v1.1.1 // indirect
|
github.com/go-test/deep v1.1.1 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
@@ -52,7 +52,7 @@ require (
|
|||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
google.golang.org/grpc v1.77.0 // indirect
|
google.golang.org/grpc v1.77.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -227,8 +227,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/auth"
|
"github.com/tech/sendico/pkg/auth"
|
||||||
"github.com/tech/sendico/pkg/db/policy"
|
"github.com/tech/sendico/pkg/db/policy"
|
||||||
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PaymentMethodsDB struct {
|
type PaymentMethodsDB struct {
|
||||||
@@ -45,5 +47,13 @@ func Create(ctx context.Context,
|
|||||||
getArchivable,
|
getArchivable,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := res.DBImp.Repository.CreateIndex(&ri.Definition{
|
||||||
|
Keys: []ri.Key{{Field: "recipientRef", Sort: ri.Asc}},
|
||||||
|
}); err != nil {
|
||||||
|
res.DBImp.Logger.Error("Failed to create recipientRef index for payment methods", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/auth"
|
"github.com/tech/sendico/pkg/auth"
|
||||||
"github.com/tech/sendico/pkg/db/paymethod"
|
"github.com/tech/sendico/pkg/db/paymethod"
|
||||||
"github.com/tech/sendico/pkg/db/policy"
|
"github.com/tech/sendico/pkg/db/policy"
|
||||||
"github.com/tech/sendico/pkg/db/repository"
|
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
@@ -16,8 +15,7 @@ import (
|
|||||||
type RecipientDB struct {
|
type RecipientDB struct {
|
||||||
auth.ProtectedDBImp[*model.Recipient]
|
auth.ProtectedDBImp[*model.Recipient]
|
||||||
auth.ArchivableDB[*model.Recipient]
|
auth.ArchivableDB[*model.Recipient]
|
||||||
pmdb paymethod.DB
|
pmdb paymethod.DB
|
||||||
paymentMethodsRepo repository.Repository
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Create(ctx context.Context,
|
func Create(ctx context.Context,
|
||||||
@@ -27,7 +25,7 @@ func Create(ctx context.Context,
|
|||||||
pmdb paymethod.DB,
|
pmdb paymethod.DB,
|
||||||
db *mongo.Database,
|
db *mongo.Database,
|
||||||
) (*RecipientDB, error) {
|
) (*RecipientDB, error) {
|
||||||
p, err := auth.CreateDBImp[*model.Recipient](ctx, logger, pdb, enforcer, mservice.Organizations, db)
|
p, err := auth.CreateDBImp[*model.Recipient](ctx, logger, pdb, enforcer, mservice.Recipients, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -49,8 +47,7 @@ func Create(ctx context.Context,
|
|||||||
createEmpty,
|
createEmpty,
|
||||||
getArchivable,
|
getArchivable,
|
||||||
),
|
),
|
||||||
paymentMethodsRepo: repository.CreateMongoRepository(db, string(mservice.PaymentMethods)),
|
pmdb: pmdb,
|
||||||
pmdb: pmdb,
|
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ require (
|
|||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -93,6 +93,6 @@ require (
|
|||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH
|
|||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -269,8 +269,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
|||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@@ -64,8 +65,80 @@ func (t *PaymentType) UnmarshalJSON(data []byte) error {
|
|||||||
|
|
||||||
type PaymentMethod struct {
|
type PaymentMethod struct {
|
||||||
PermissionBound `bson:",inline" json:",inline"`
|
PermissionBound `bson:",inline" json:",inline"`
|
||||||
|
Describable `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
RecipientRef primitive.ObjectID `bson:"recipientRef" json:"recipientRef"`
|
RecipientRef primitive.ObjectID `bson:"recipientRef" json:"recipientRef"`
|
||||||
Type PaymentType `bson:"type" json:"type"`
|
Type PaymentType `bson:"type" json:"type"`
|
||||||
Data bson.Raw `bson:"data" json:"data"`
|
Data bson.Raw `bson:"data" json:"data"`
|
||||||
|
IsMain bool `bson:"isMain" json:"isMain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type paymentMethodJSON struct {
|
||||||
|
PermissionBound `json:",inline"`
|
||||||
|
Describable `json:",inline"`
|
||||||
|
|
||||||
|
RecipientRef primitive.ObjectID `json:"recipientRef"`
|
||||||
|
Type PaymentType `json:"type"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
IsMain bool `json:"isMain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m PaymentMethod) MarshalJSON() ([]byte, error) {
|
||||||
|
var marshaledData json.RawMessage
|
||||||
|
|
||||||
|
if len(m.Data) > 0 {
|
||||||
|
var data bson.M
|
||||||
|
if err := bson.Unmarshal(m.Data, &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if data != nil {
|
||||||
|
b, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
marshaledData = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := paymentMethodJSON{
|
||||||
|
PermissionBound: m.PermissionBound,
|
||||||
|
Describable: m.Describable,
|
||||||
|
RecipientRef: m.RecipientRef,
|
||||||
|
Type: m.Type,
|
||||||
|
Data: marshaledData,
|
||||||
|
IsMain: m.IsMain,
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PaymentMethod) UnmarshalJSON(data []byte) error {
|
||||||
|
var payload paymentMethodJSON
|
||||||
|
if err := json.Unmarshal(data, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.PermissionBound = payload.PermissionBound
|
||||||
|
m.Describable = payload.Describable
|
||||||
|
m.RecipientRef = payload.RecipientRef
|
||||||
|
m.Type = payload.Type
|
||||||
|
m.IsMain = payload.IsMain
|
||||||
|
|
||||||
|
if len(payload.Data) == 0 || bytes.Equal(payload.Data, []byte("null")) {
|
||||||
|
m.Data = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawData map[string]any
|
||||||
|
if err := json.Unmarshal(payload.Data, &rawData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := bson.Marshal(rawData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Data = raw
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ replace github.com/tech/sendico/ledger => ../ledger
|
|||||||
replace github.com/tech/sendico/gateway/chain => ../gateway/chain
|
replace github.com/tech/sendico/gateway/chain => ../gateway/chain
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0
|
github.com/aws/aws-sdk-go-v2 v1.40.1
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.2
|
github.com/aws/aws-sdk-go-v2/config v1.32.3
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.3
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-chi/jwtauth/v5 v5.3.3
|
github.com/go-chi/jwtauth/v5 v5.3.3
|
||||||
@@ -43,21 +43,21 @@ require (
|
|||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 // indirect
|
||||||
github.com/aws/smithy-go v1.23.2 // indirect
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
|
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
@@ -79,7 +79,7 @@ require (
|
|||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||||
@@ -135,6 +135,6 @@ require (
|
|||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
google.golang.org/grpc v1.77.0 // indirect
|
google.golang.org/grpc v1.77.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,44 +6,44 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
|
github.com/aws/aws-sdk-go-v2/config v1.32.3 h1:cpz7H2uMNTDa0h/5CYL5dLUEzPSLo2g0NkbxTRJtSSU=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
|
github.com/aws/aws-sdk-go-v2/config v1.32.3/go.mod h1:srtPKaJJe3McW6T/+GMBZyIPc+SeqJsNPJsd4mOYZ6s=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.3 h1:01Ym72hK43hjwDeJUfi1l2oYLXBAOR8gNSZNmXmvuas=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.3/go.mod h1:55nWF/Sr9Zvls0bGnWkRxUdhzKqj9uRNlPvgV1vgxKc=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 h1:utxLraaifrSBkeyII9mIbVwXXWrZdlPO7FIKmyLCEcY=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15/go.mod h1:hW6zjYUDQwfz3icf4g2O41PHi77u10oAzJ84iSzR/lo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 h1:Y5YXgygXwDI5P4RkteB5yF7v35neH7LfJKBG+hzIons=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15/go.mod h1:K+/1EpG42dFSY7CBj+Fruzm8PsCGWTXJ3jdeJ659oGQ=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 h1:AvltKnW9ewxX2hFmQS0FyJH93aSvJVUEFvXfU+HWtSE=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15/go.mod h1:3I4oCdZdmgrREhU74qS1dK9yZ62yumob+58AbFR4cQA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15 h1:NLYTEyZmVZo0Qh183sC8nC+ydJXOOeIL/qI/sS3PdLY=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15/go.mod h1:Z803iB3B0bc8oJV8zH2PERLRfQUJ2n2BXISpsA4+O1M=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6 h1:P1MU/SuhadGvg2jtviDXPEejU3jBNhoeeAlRadHzvHI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6/go.mod h1:5KYaMG6wmVKMFBSfWoyG/zH8pWwzQFnKgpoSRlXHKdQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 h1:3/u/4yZOffg5jdNk1sDpOQ4Y+R6Xbh+GzpDrSZjuy3U=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15/go.mod h1:4Zkjq0FKjE78NKjabuM4tRXKFzUJWXgP0ItEZK8l7JU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15 h1:wsSQ4SVz5YE1crz0Ap7VBZrV4nNqZt4CIBBT8mnwoNc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15/go.mod h1:I7sditnFGtYMIqPRU1QoHZAUrXkGp4SczmlLwrNPlD0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0 h1:IrbE3B8O9pm3lsg96AXIN5MXX4pECEuExh/A0Du3AuI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0/go.mod h1:/sJLzHtiiZvs6C1RbxS/anSAFwZD6oC6M/kotQzOiLw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 h1:d/6xOGIllc/XW1lzG9a4AUBMmpLA9PXcQnVPTuHHcik=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.3/go.mod h1:fQ7E7Qj9GiW8y0ClD7cUJk3Bz5Iw8wZkWDHsTe8vDKs=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 h1:8sTTiw+9yuNXcfWeqKF2x01GqCF49CpP4Z9nKrrk/ts=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.6/go.mod h1:8WYg+Y40Sn3X2hioaaWAAIngndR8n1XFdRPPX+7QBaM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 h1:E+KqWoVsSrj1tJ6I/fjDIu5xoS2Zacuu1zT+H7KtiIk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11/go.mod h1:qyWHz+4lvkXcr3+PoGlGHEI+3DLLiU6/GdrFfMaAhB0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 h1:tzMkjh0yTChUqJDgGkcDdxvZDSrJ/WB6R6ymI5ehqJI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.3/go.mod h1:T270C0R5sZNLbWUe8ueiAF42XSZxxPocTaGSgs5c/60=
|
||||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
@@ -123,8 +123,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5uk
|
|||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -359,8 +359,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
|||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package recipient
|
|||||||
import (
|
import (
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
"github.com/tech/sendico/server/interface/api"
|
"github.com/tech/sendico/server/interface/api"
|
||||||
recipientimp "github.com/tech/sendico/server/internal/server/paymethodsimp"
|
"github.com/tech/sendico/server/internal/server/recipientimp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Create(a api.API) (mservice.MicroService, error) {
|
func Create(a api.API) (mservice.MicroService, error) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type RecipientAPI struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *RecipientAPI) Name() mservice.Type {
|
func (a *RecipientAPI) Name() mservice.Type {
|
||||||
return mservice.Recipients
|
return mservice.PaymentMethods
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *RecipientAPI) Finish(_ context.Context) error {
|
func (a *RecipientAPI) Finish(_ context.Context) error {
|
||||||
|
|||||||
@@ -66,11 +66,10 @@ MNTX_GATEWAY_DIR=mntx_gateway
|
|||||||
MNTX_GATEWAY_COMPOSE_PROJECT=sendico-mntx-gateway
|
MNTX_GATEWAY_COMPOSE_PROJECT=sendico-mntx-gateway
|
||||||
MNTX_GATEWAY_SERVICE_NAME=sendico_mntx_gateway
|
MNTX_GATEWAY_SERVICE_NAME=sendico_mntx_gateway
|
||||||
MNTX_GATEWAY_GRPC_PORT=50075
|
MNTX_GATEWAY_GRPC_PORT=50075
|
||||||
MNTX_GATEWAY_METRICS_PORT=9404
|
MNTX_GATEWAY_METRICS_PORT=9405
|
||||||
MNTX_GATEWAY_HTTP_PORT=8080
|
MNTX_GATEWAY_HTTP_PORT=8084
|
||||||
MONETIX_BASE_URL=https://api.txflux.com
|
MONETIX_BASE_URL=https://api.txflux.com
|
||||||
|
|
||||||
|
|
||||||
# FX oracle stack
|
# FX oracle stack
|
||||||
FX_ORACLE_DIR=fx_oracle
|
FX_ORACLE_DIR=fx_oracle
|
||||||
FX_ORACLE_COMPOSE_PROJECT=sendico-fx-oracle
|
FX_ORACLE_COMPOSE_PROJECT=sendico-fx-oracle
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ services:
|
|||||||
- "0.0.0.0:${FRONTEND_HTTP_PORT}:80"
|
- "0.0.0.0:${FRONTEND_HTTP_PORT}:80"
|
||||||
- "0.0.0.0:${FRONTEND_HTTPS_PORT}:443"
|
- "0.0.0.0:${FRONTEND_HTTPS_PORT}:443"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL","curl -sf http://localhost:80/ >/dev/null"]
|
test: ["CMD", "curl", "-f", "http://localhost:2019/config"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -37,6 +37,6 @@ COPY api/gateway/mntx/config.yml /app/config.yml
|
|||||||
COPY api/gateway/mntx/entrypoint.sh /app/entrypoint.sh
|
COPY api/gateway/mntx/entrypoint.sh /app/entrypoint.sh
|
||||||
COPY --from=build /out/mntx-gateway /app/mntx-gateway
|
COPY --from=build /out/mntx-gateway /app/mntx-gateway
|
||||||
RUN chmod +x /app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh
|
||||||
EXPOSE 50075 9404 8080
|
EXPOSE 50075 9405 8084
|
||||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
CMD ["/app/mntx-gateway","--config.file","/app/config.yml"]
|
CMD ["/app/mntx-gateway","--config.file","/app/config.yml"]
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ services:
|
|||||||
command: ["--config.file", "/app/config.yml"]
|
command: ["--config.file", "/app/config.yml"]
|
||||||
ports:
|
ports:
|
||||||
- "0.0.0.0:${MNTX_GATEWAY_GRPC_PORT:-50075}:50075"
|
- "0.0.0.0:${MNTX_GATEWAY_GRPC_PORT:-50075}:50075"
|
||||||
- "0.0.0.0:${MNTX_GATEWAY_METRICS_PORT:-9404}:9404"
|
- "0.0.0.0:${MNTX_GATEWAY_METRICS_PORT:-9405}:9405"
|
||||||
- "0.0.0.0:${MNTX_GATEWAY_HTTP_PORT:-8080}:8080"
|
- "0.0.0.0:${MNTX_GATEWAY_HTTP_PORT:-8084}:8084"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL","wget -qO- http://localhost:9404/health | grep -q '\"status\":\"ok\"'"]
|
test: ["CMD-SHELL","wget -qO- http://localhost:9405/health | grep -q '\"status\":\"ok\"'"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
21
frontend/pshared/lib/api/responses/payment_method.dart
Normal file
21
frontend/pshared/lib/api/responses/payment_method.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/api/responses/base.dart';
|
||||||
|
import 'package:pshared/api/responses/token.dart';
|
||||||
|
import 'package:pshared/data/dto/payment/method.dart';
|
||||||
|
|
||||||
|
part 'payment_method.g.dart';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonSerializable(explicitToJson: true)
|
||||||
|
class PaymentMethodResponse extends BaseAuthorizedResponse {
|
||||||
|
|
||||||
|
@JsonKey(name: 'payment_methods')
|
||||||
|
final List<PaymentMethodDTO> paymentMethods;
|
||||||
|
|
||||||
|
const PaymentMethodResponse({required super.accessToken, required this.paymentMethods});
|
||||||
|
|
||||||
|
factory PaymentMethodResponse.fromJson(Map<String, dynamic> json) => _$PaymentMethodResponseFromJson(json);
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => _$PaymentMethodResponseToJson(this);
|
||||||
|
}
|
||||||
19
frontend/pshared/lib/api/responses/recipient.dart
Normal file
19
frontend/pshared/lib/api/responses/recipient.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/api/responses/base.dart';
|
||||||
|
import 'package:pshared/api/responses/token.dart';
|
||||||
|
import 'package:pshared/data/dto/recipient/recipient.dart';
|
||||||
|
|
||||||
|
part 'recipient.g.dart';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonSerializable(explicitToJson: true)
|
||||||
|
class RecipientResponse extends BaseAuthorizedResponse {
|
||||||
|
final List<RecipientDTO> recipients;
|
||||||
|
|
||||||
|
const RecipientResponse({required super.accessToken, required this.recipients});
|
||||||
|
|
||||||
|
factory RecipientResponse.fromJson(Map<String, dynamic> json) => _$RecipientResponseFromJson(json);
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => _$RecipientResponseToJson(this);
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ part 'method.g.dart';
|
|||||||
class PaymentMethodDTO extends PermissionBoundDTO {
|
class PaymentMethodDTO extends PermissionBoundDTO {
|
||||||
final String recipientRef;
|
final String recipientRef;
|
||||||
final String type;
|
final String type;
|
||||||
|
final String name;
|
||||||
|
final String? description;
|
||||||
|
final bool isMain;
|
||||||
final Map<String, dynamic> data;
|
final Map<String, dynamic> data;
|
||||||
|
|
||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
@@ -24,6 +27,9 @@ class PaymentMethodDTO extends PermissionBoundDTO {
|
|||||||
required this.recipientRef,
|
required this.recipientRef,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.data,
|
required this.data,
|
||||||
|
required this.name,
|
||||||
|
required this.isMain,
|
||||||
|
this.description,
|
||||||
this.isArchived = false,
|
this.isArchived = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,21 @@ import 'package:pshared/data/mapper/payment/iban.dart';
|
|||||||
import 'package:pshared/data/mapper/payment/russian_bank.dart';
|
import 'package:pshared/data/mapper/payment/russian_bank.dart';
|
||||||
import 'package:pshared/data/mapper/payment/type.dart';
|
import 'package:pshared/data/mapper/payment/type.dart';
|
||||||
import 'package:pshared/data/mapper/payment/wallet.dart';
|
import 'package:pshared/data/mapper/payment/wallet.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/organization/bound.dart';
|
import 'package:pshared/models/organization/bound.dart';
|
||||||
import 'package:pshared/models/payment/methods/card.dart';
|
import 'package:pshared/models/payment/methods/card.dart';
|
||||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||||
import 'package:pshared/models/payment/methods/data.dart';
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
import 'package:pshared/models/payment/methods/iban.dart';
|
import 'package:pshared/models/payment/methods/iban.dart';
|
||||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/type.dart';
|
||||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||||
import 'package:pshared/models/payment/payment_method.dart';
|
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:pshared/models/payment/type.dart';
|
||||||
import 'package:pshared/models/permissions/bound.dart';
|
import 'package:pshared/models/permissions/bound.dart';
|
||||||
import 'package:pshared/models/storable.dart';
|
import 'package:pshared/models/storable.dart';
|
||||||
|
|
||||||
|
|
||||||
extension PaymentMethodModelMapper on PaymentMethodModel {
|
extension PaymentMethodMapper on PaymentMethod {
|
||||||
PaymentMethodDTO toDTO() => PaymentMethodDTO(
|
PaymentMethodDTO toDTO() => PaymentMethodDTO(
|
||||||
id: storable.id,
|
id: storable.id,
|
||||||
createdAt: storable.createdAt,
|
createdAt: storable.createdAt,
|
||||||
@@ -34,6 +35,9 @@ extension PaymentMethodModelMapper on PaymentMethodModel {
|
|||||||
type: paymentTypeToValue(type),
|
type: paymentTypeToValue(type),
|
||||||
data: _dataToJson(data),
|
data: _dataToJson(data),
|
||||||
isArchived: isArchived,
|
isArchived: isArchived,
|
||||||
|
name: describable.name,
|
||||||
|
description: describable.description,
|
||||||
|
isMain: isMain,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _dataToJson(PaymentMethodData data) {
|
Map<String, dynamic> _dataToJson(PaymentMethodData data) {
|
||||||
@@ -53,7 +57,7 @@ extension PaymentMethodModelMapper on PaymentMethodModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension PaymentMethodDTOMapper on PaymentMethodDTO {
|
extension PaymentMethodDTOMapper on PaymentMethodDTO {
|
||||||
PaymentMethodModel toDomain() => PaymentMethodModel(
|
PaymentMethod toDomain() => PaymentMethod(
|
||||||
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
|
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
|
||||||
permissionBound: newPermissionBound(
|
permissionBound: newPermissionBound(
|
||||||
organizationBound: newOrganizationBound(organizationRef: organizationRef),
|
organizationBound: newOrganizationBound(organizationRef: organizationRef),
|
||||||
@@ -62,6 +66,8 @@ extension PaymentMethodDTOMapper on PaymentMethodDTO {
|
|||||||
recipientRef: recipientRef,
|
recipientRef: recipientRef,
|
||||||
data: _dataToDomain(paymentTypeFromValue(type), data),
|
data: _dataToDomain(paymentTypeFromValue(type), data),
|
||||||
isArchived: isArchived,
|
isArchived: isArchived,
|
||||||
|
describable: newDescribable(name: name, description: description),
|
||||||
|
isMain: isMain,
|
||||||
);
|
);
|
||||||
|
|
||||||
PaymentMethodData _dataToDomain(PaymentType paymentType, Map<String, dynamic> payload) {
|
PaymentMethodData _dataToDomain(PaymentType paymentType, Map<String, dynamic> payload) {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import 'package:pshared/data/dto/recipient/recipient.dart';
|
|||||||
import 'package:pshared/models/describable.dart';
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/organization/bound.dart';
|
import 'package:pshared/models/organization/bound.dart';
|
||||||
import 'package:pshared/models/permissions/bound.dart';
|
import 'package:pshared/models/permissions/bound.dart';
|
||||||
import 'package:pshared/models/recipient/recipient_model.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
import 'package:pshared/models/recipient/status.dart';
|
import 'package:pshared/models/recipient/status.dart';
|
||||||
import 'package:pshared/models/recipient/type.dart';
|
import 'package:pshared/models/recipient/type.dart';
|
||||||
import 'package:pshared/models/storable.dart';
|
import 'package:pshared/models/storable.dart';
|
||||||
|
|
||||||
|
|
||||||
extension RecipientModelMapper on RecipientModel {
|
extension RecipientModelMapper on Recipient {
|
||||||
RecipientDTO toDTO() => RecipientDTO(
|
RecipientDTO toDTO() => RecipientDTO(
|
||||||
id: storable.id,
|
id: storable.id,
|
||||||
createdAt: storable.createdAt,
|
createdAt: storable.createdAt,
|
||||||
@@ -26,7 +26,7 @@ extension RecipientModelMapper on RecipientModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension RecipientDTOMapper on RecipientDTO {
|
extension RecipientDTOMapper on RecipientDTO {
|
||||||
RecipientModel toDomain() => RecipientModel(
|
Recipient toDomain() => Recipient(
|
||||||
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
|
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
|
||||||
permissionBound: newPermissionBound(
|
permissionBound: newPermissionBound(
|
||||||
organizationBound: newOrganizationBound(organizationRef: organizationRef),
|
organizationBound: newOrganizationBound(organizationRef: organizationRef),
|
||||||
|
|||||||
@@ -19,5 +19,15 @@
|
|||||||
"operationStatusError": "Error",
|
"operationStatusError": "Error",
|
||||||
"@operationStatusError": {
|
"@operationStatusError": {
|
||||||
"description": "Label for the “error” operation status"
|
"description": "Label for the “error” operation status"
|
||||||
|
},
|
||||||
|
|
||||||
|
"resourceLoadError": "Error while loading data. Try again",
|
||||||
|
"@resourceLoadError": {
|
||||||
|
"description": "Default message shown when data loading fails"
|
||||||
|
},
|
||||||
|
|
||||||
|
"resourceEmpty": "Empty data",
|
||||||
|
"@resourceEmpty": {
|
||||||
|
"description": "Default message shown when no data is available"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,15 @@
|
|||||||
"operationStatusError": "Ошибка",
|
"operationStatusError": "Ошибка",
|
||||||
"@operationStatusError": {
|
"@operationStatusError": {
|
||||||
"description": "Label for the “error” operation status"
|
"description": "Label for the “error” operation status"
|
||||||
|
},
|
||||||
|
|
||||||
|
"resourceLoadError": "Ошибка при загрузке данных. Попробуйте еще раз",
|
||||||
|
"@resourceLoadError": {
|
||||||
|
"description": "Default message shown when data loading fails"
|
||||||
|
},
|
||||||
|
|
||||||
|
"resourceEmpty": "Нет данных",
|
||||||
|
"@resourceEmpty": {
|
||||||
|
"description": "Default message shown when no data is available"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,6 @@ import 'package:pshared/models/payment/type.dart';
|
|||||||
|
|
||||||
abstract class PaymentMethodData {
|
abstract class PaymentMethodData {
|
||||||
PaymentType get type;
|
PaymentType get type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef MethodMap = Map<PaymentType, PaymentMethodData?>;
|
||||||
@@ -1,21 +1,79 @@
|
|||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/card.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/iban.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:pshared/models/payment/type.dart';
|
||||||
|
import 'package:pshared/models/permissions/bound.dart';
|
||||||
|
import 'package:pshared/models/permissions/bound/storable.dart';
|
||||||
|
import 'package:pshared/models/storable.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethod {
|
class PaymentMethod implements PermissionBoundStorable, Describable {
|
||||||
PaymentMethod({
|
final Storable storable;
|
||||||
required this.id,
|
final PermissionBound permissionBound;
|
||||||
required this.label,
|
final Describable describable;
|
||||||
required this.details,
|
final String recipientRef;
|
||||||
required this.type,
|
final PaymentMethodData data;
|
||||||
this.isEnabled = true,
|
final bool isArchived;
|
||||||
|
final bool isMain;
|
||||||
|
|
||||||
|
const PaymentMethod({
|
||||||
|
required this.storable,
|
||||||
|
required this.permissionBound,
|
||||||
|
required this.describable,
|
||||||
|
required this.recipientRef,
|
||||||
|
required this.data,
|
||||||
|
this.isArchived = false,
|
||||||
this.isMain = false,
|
this.isMain = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
PaymentType get type => data.type;
|
||||||
final String label;
|
|
||||||
final String details;
|
|
||||||
final PaymentType type;
|
|
||||||
|
|
||||||
bool isEnabled;
|
T dataAs<T extends PaymentMethodData>() {
|
||||||
bool isMain;
|
final currentData = data;
|
||||||
}
|
if (currentData is T) return currentData;
|
||||||
|
throw StateError('Payment method data is ${currentData.runtimeType}, requested $T for type $type');
|
||||||
|
}
|
||||||
|
T? dataAsOrNull<T extends PaymentMethodData>() => data is T ? data as T : null;
|
||||||
|
|
||||||
|
CardPaymentMethod? get cardData => dataAsOrNull<CardPaymentMethod>();
|
||||||
|
IbanPaymentMethod? get ibanData => dataAsOrNull<IbanPaymentMethod>();
|
||||||
|
RussianBankAccountPaymentMethod? get bankAccountData => dataAsOrNull<RussianBankAccountPaymentMethod>();
|
||||||
|
WalletPaymentMethod? get walletData => dataAsOrNull<WalletPaymentMethod>();
|
||||||
|
CryptoAddressPaymentMethod? get cryptoAddressData => dataAsOrNull<CryptoAddressPaymentMethod>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => storable.id;
|
||||||
|
@override
|
||||||
|
DateTime get createdAt => storable.createdAt;
|
||||||
|
@override
|
||||||
|
DateTime get updatedAt => storable.updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get organizationRef => permissionBound.organizationRef;
|
||||||
|
@override
|
||||||
|
String get permissionRef => permissionBound.permissionRef;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => describable.name;
|
||||||
|
@override
|
||||||
|
String? get description => describable.description;
|
||||||
|
|
||||||
|
PaymentMethod copyWith({
|
||||||
|
PaymentMethodData? data,
|
||||||
|
bool? isArchived,
|
||||||
|
bool? isMain,
|
||||||
|
Describable? describable,
|
||||||
|
}) => PaymentMethod(
|
||||||
|
storable: storable,
|
||||||
|
permissionBound: permissionBound,
|
||||||
|
recipientRef: recipientRef,
|
||||||
|
data: data ?? this.data,
|
||||||
|
isArchived: isArchived ?? this.isArchived,
|
||||||
|
isMain: isMain ?? this.isMain,
|
||||||
|
describable: describable ?? this.describable,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import 'package:pshared/models/payment/methods/data.dart';
|
|
||||||
import 'package:pshared/models/payment/type.dart';
|
|
||||||
import 'package:pshared/models/permissions/bound.dart';
|
|
||||||
import 'package:pshared/models/storable.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodModel implements PermissionBound, Storable {
|
|
||||||
final Storable storable;
|
|
||||||
final PermissionBound permissionBound;
|
|
||||||
final String recipientRef;
|
|
||||||
final PaymentMethodData data;
|
|
||||||
final bool isArchived;
|
|
||||||
|
|
||||||
const PaymentMethodModel({
|
|
||||||
required this.storable,
|
|
||||||
required this.permissionBound,
|
|
||||||
required this.recipientRef,
|
|
||||||
required this.data,
|
|
||||||
this.isArchived = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
PaymentType get type => data.type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get id => storable.id;
|
|
||||||
@override
|
|
||||||
DateTime get createdAt => storable.createdAt;
|
|
||||||
@override
|
|
||||||
DateTime get updatedAt => storable.updatedAt;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get organizationRef => permissionBound.organizationRef;
|
|
||||||
@override
|
|
||||||
String get permissionRef => permissionBound.permissionRef;
|
|
||||||
|
|
||||||
PaymentMethodModel copyWith({
|
|
||||||
PaymentMethodData? data,
|
|
||||||
bool? isArchived,
|
|
||||||
}) => PaymentMethodModel(
|
|
||||||
storable: storable,
|
|
||||||
permissionBound: permissionBound,
|
|
||||||
recipientRef: recipientRef,
|
|
||||||
data: data ?? this.data,
|
|
||||||
isArchived: isArchived ?? this.isArchived,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,86 +1,96 @@
|
|||||||
import 'package:pshared/models/payment/methods/card.dart';
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/payment/methods/iban.dart';
|
import 'package:pshared/models/organization/bound.dart';
|
||||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
import 'package:pshared/models/permissions/bound/describable.dart';
|
||||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
import 'package:pshared/models/permissions/bound.dart';
|
||||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
|
||||||
import 'package:pshared/models/recipient/status.dart';
|
import 'package:pshared/models/recipient/status.dart';
|
||||||
import 'package:pshared/models/recipient/type.dart';
|
import 'package:pshared/models/recipient/type.dart';
|
||||||
|
import 'package:pshared/models/storable.dart';
|
||||||
|
|
||||||
|
|
||||||
class Recipient {
|
class Recipient implements PermissionBoundStorableDescribable {
|
||||||
final String? avatarUrl; // network URL / local asset
|
final Storable storable;
|
||||||
final String name;
|
final PermissionBound permissionBound;
|
||||||
|
final Describable describable;
|
||||||
final String email;
|
final String email;
|
||||||
|
final String? avatarUrl;
|
||||||
final RecipientStatus status;
|
final RecipientStatus status;
|
||||||
final RecipientType type;
|
final RecipientType type;
|
||||||
final CardPaymentMethod? card;
|
final bool isArchived;
|
||||||
final IbanPaymentMethod? iban;
|
|
||||||
final RussianBankAccountPaymentMethod? bank;
|
|
||||||
final WalletPaymentMethod? wallet;
|
|
||||||
final CryptoAddressPaymentMethod? cryptoAddress;
|
|
||||||
|
|
||||||
const Recipient({
|
const Recipient({
|
||||||
this.avatarUrl,
|
required this.storable,
|
||||||
required this.name,
|
required this.permissionBound,
|
||||||
|
required this.describable,
|
||||||
required this.email,
|
required this.email,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.type,
|
required this.type,
|
||||||
this.card,
|
this.avatarUrl,
|
||||||
this.iban,
|
this.isArchived = false,
|
||||||
this.bank,
|
|
||||||
this.wallet,
|
|
||||||
this.cryptoAddress,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Convenience factory for quickly creating mock recipients.
|
@override
|
||||||
factory Recipient.mock({
|
String get id => storable.id;
|
||||||
required String name,
|
@override
|
||||||
required String email,
|
DateTime get createdAt => storable.createdAt;
|
||||||
required RecipientStatus status,
|
@override
|
||||||
required RecipientType type,
|
DateTime get updatedAt => storable.updatedAt;
|
||||||
CardPaymentMethod? card,
|
|
||||||
IbanPaymentMethod? iban,
|
|
||||||
RussianBankAccountPaymentMethod? bank,
|
|
||||||
WalletPaymentMethod? wallet,
|
|
||||||
CryptoAddressPaymentMethod? cryptoAddress,
|
|
||||||
}) =>
|
|
||||||
Recipient(
|
|
||||||
avatarUrl: null,
|
|
||||||
name: name,
|
|
||||||
email: email,
|
|
||||||
status: status,
|
|
||||||
type: type,
|
|
||||||
card: card,
|
|
||||||
iban: iban,
|
|
||||||
bank: bank,
|
|
||||||
wallet: wallet,
|
|
||||||
cryptoAddress: cryptoAddress,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get organizationRef => permissionBound.organizationRef;
|
||||||
|
@override
|
||||||
|
String get permissionRef => permissionBound.permissionRef;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => describable.name;
|
||||||
|
@override
|
||||||
|
String? get description => describable.description;
|
||||||
|
|
||||||
|
Recipient copyWith({
|
||||||
|
Describable? describable,
|
||||||
|
String? email,
|
||||||
|
String? Function()? avatarUrl,
|
||||||
|
RecipientStatus? status,
|
||||||
|
RecipientType? type,
|
||||||
|
bool? isArchived,
|
||||||
|
}) => Recipient(
|
||||||
|
storable: storable,
|
||||||
|
permissionBound: permissionBound,
|
||||||
|
describable: describableCopyWithOther(this.describable, describable),
|
||||||
|
email: email ?? this.email,
|
||||||
|
avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
|
||||||
|
status: status ?? this.status,
|
||||||
|
type: type ?? this.type,
|
||||||
|
isArchived: isArchived ?? this.isArchived,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: move search to backend
|
||||||
bool matchesQuery(String q) {
|
bool matchesQuery(String q) {
|
||||||
final searchable = [
|
final searchable = [
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
card?.pan,
|
|
||||||
card?.firstName,
|
|
||||||
card?.lastName,
|
|
||||||
iban?.iban,
|
|
||||||
iban?.accountHolder,
|
|
||||||
iban?.bic,
|
|
||||||
iban?.bankName,
|
|
||||||
bank?.accountNumber,
|
|
||||||
bank?.recipientName,
|
|
||||||
bank?.inn,
|
|
||||||
bank?.kpp,
|
|
||||||
bank?.bankName,
|
|
||||||
bank?.bik,
|
|
||||||
bank?.correspondentAccount,
|
|
||||||
wallet?.walletId,
|
|
||||||
cryptoAddress?.address,
|
|
||||||
cryptoAddress?.network,
|
|
||||||
cryptoAddress?.destinationTag,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return searchable.any((field) => field?.toLowerCase().contains(q) ?? false);
|
return searchable.any((field) => field.toLowerCase().contains(q.toLowerCase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Recipient newRecipient({
|
||||||
|
required String organizationRef,
|
||||||
|
required String email,
|
||||||
|
required String name,
|
||||||
|
required RecipientStatus status,
|
||||||
|
required RecipientType type,
|
||||||
|
String? description,
|
||||||
|
String? avatarUrl,
|
||||||
|
bool isArchived = false,
|
||||||
|
}) => Recipient(
|
||||||
|
storable: newStorable(),
|
||||||
|
permissionBound: newPermissionBound(organizationBound: newOrganizationBound(organizationRef: organizationRef)),
|
||||||
|
describable: newDescribable(name: name, description: description),
|
||||||
|
email: email,
|
||||||
|
status: status,
|
||||||
|
type: type,
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
isArchived: isArchived,
|
||||||
|
);
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import 'package:pshared/models/describable.dart';
|
|
||||||
import 'package:pshared/models/permissions/bound/describable.dart';
|
|
||||||
import 'package:pshared/models/permissions/bound.dart';
|
|
||||||
import 'package:pshared/models/recipient/status.dart';
|
|
||||||
import 'package:pshared/models/recipient/type.dart';
|
|
||||||
import 'package:pshared/models/storable.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class RecipientModel implements PermissionBoundStorableDescribable {
|
|
||||||
final Storable storable;
|
|
||||||
final PermissionBound permissionBound;
|
|
||||||
final Describable describable;
|
|
||||||
final String email;
|
|
||||||
final String? avatarUrl;
|
|
||||||
final RecipientStatus status;
|
|
||||||
final RecipientType type;
|
|
||||||
final bool isArchived;
|
|
||||||
|
|
||||||
const RecipientModel({
|
|
||||||
required this.storable,
|
|
||||||
required this.permissionBound,
|
|
||||||
required this.describable,
|
|
||||||
required this.email,
|
|
||||||
required this.status,
|
|
||||||
required this.type,
|
|
||||||
this.avatarUrl,
|
|
||||||
this.isArchived = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get id => storable.id;
|
|
||||||
@override
|
|
||||||
DateTime get createdAt => storable.createdAt;
|
|
||||||
@override
|
|
||||||
DateTime get updatedAt => storable.updatedAt;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get organizationRef => permissionBound.organizationRef;
|
|
||||||
@override
|
|
||||||
String get permissionRef => permissionBound.permissionRef;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get name => describable.name;
|
|
||||||
@override
|
|
||||||
String? get description => describable.description;
|
|
||||||
|
|
||||||
RecipientModel copyWith({
|
|
||||||
Describable? describable,
|
|
||||||
String? email,
|
|
||||||
String? Function()? avatarUrl,
|
|
||||||
RecipientStatus? status,
|
|
||||||
RecipientType? type,
|
|
||||||
bool? isArchived,
|
|
||||||
}) => RecipientModel(
|
|
||||||
storable: storable,
|
|
||||||
permissionBound: permissionBound,
|
|
||||||
describable: describableCopyWithOther(this.describable, describable),
|
|
||||||
email: email ?? this.email,
|
|
||||||
avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
|
|
||||||
status: status ?? this.status,
|
|
||||||
type: type ?? this.type,
|
|
||||||
isArchived: isArchived ?? this.isArchived,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
|
||||||
/// Represents various resource types (mirroring your Go "Type" constants).
|
/// Represents various resource types (mirroring your Go "Type" constants).
|
||||||
enum ResourceType {
|
enum ResourceType {
|
||||||
/// Represents user accounts in the system
|
/// Represents user accounts in the system
|
||||||
@@ -75,6 +76,12 @@ enum ResourceType {
|
|||||||
@JsonValue('ledger_posing_lines')
|
@JsonValue('ledger_posing_lines')
|
||||||
ledgerPostingLines,
|
ledgerPostingLines,
|
||||||
|
|
||||||
|
@JsonValue('payments')
|
||||||
|
payments,
|
||||||
|
|
||||||
|
@JsonValue('payment_methods')
|
||||||
|
paymentMethods,
|
||||||
|
|
||||||
/// Represents permissions service
|
/// Represents permissions service
|
||||||
@JsonValue('permissions')
|
@JsonValue('permissions')
|
||||||
permissions,
|
permissions,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
Resource<Account?> get resource => _resource;
|
Resource<Account?> get resource => _resource;
|
||||||
late LocaleProvider _localeProvider;
|
late LocaleProvider _localeProvider;
|
||||||
PendingLogin? _pendingLogin;
|
PendingLogin? _pendingLogin;
|
||||||
|
Future<void>? _restoreFuture;
|
||||||
|
|
||||||
Account? get account => _resource.data;
|
Account? get account => _resource.data;
|
||||||
PendingLogin? get pendingLogin => _pendingLogin;
|
PendingLogin? get pendingLogin => _pendingLogin;
|
||||||
@@ -34,6 +35,7 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
bool get isLoading => _resource.isLoading;
|
bool get isLoading => _resource.isLoading;
|
||||||
Object? get error => _resource.error;
|
Object? get error => _resource.error;
|
||||||
bool get isReady => (!isLoading) && (account != null);
|
bool get isReady => (!isLoading) && (account != null);
|
||||||
|
Future<void>? get restoreFuture => _restoreFuture;
|
||||||
|
|
||||||
Account? currentUser() {
|
Account? currentUser() {
|
||||||
final acc = account;
|
final acc = account;
|
||||||
@@ -220,4 +222,12 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> restoreIfPossible() {
|
||||||
|
return _restoreFuture ??= () async {
|
||||||
|
final hasAuth = await AuthorizationService.isAuthorizationStored();
|
||||||
|
if (!hasAuth) return;
|
||||||
|
await restore();
|
||||||
|
}();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
84
frontend/pshared/lib/provider/recipient/pmethods.dart
Normal file
84
frontend/pshared/lib/provider/recipient/pmethods.dart
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/data/mapper/payment/method.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
|
import 'package:pshared/models/organization/bound.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/type.dart';
|
||||||
|
import 'package:pshared/models/permissions/bound.dart';
|
||||||
|
import 'package:pshared/models/storable.dart';
|
||||||
|
import 'package:pshared/provider/organizations.dart';
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
import 'package:pshared/provider/template.dart';
|
||||||
|
import 'package:pshared/service/recipient/pmethods.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentMethodsProvider extends GenericProvider<PaymentMethod> {
|
||||||
|
late OrganizationsProvider _organizations;
|
||||||
|
|
||||||
|
PaymentMethodsProvider() : super(service: PaymentMethodService.basicService);
|
||||||
|
|
||||||
|
List<PaymentMethod> get methods => List<PaymentMethod>.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt)));
|
||||||
|
|
||||||
|
void updateProviders(OrganizationsProvider organizations, RecipientsProvider recipients) {
|
||||||
|
if (recipients.currentObject != null) loadMethods(organizations, recipients.currentObject?.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: current providers structure forces to use weird construction with in-place PaymentMethodsProviders
|
||||||
|
// it would be better to load reipients together with their payment methods in RecipientsProvider
|
||||||
|
Future<void> loadMethods(OrganizationsProvider organizations, String? recipientRef) async {
|
||||||
|
_organizations = organizations;
|
||||||
|
if (_organizations.isOrganizationSet && (recipientRef != null)) {
|
||||||
|
return load(_organizations.current.id, recipientRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// void reorderMethods(int oldIndex, int newIndex) {
|
||||||
|
// if (newIndex > oldIndex) newIndex--;
|
||||||
|
// final item = _methods.removeAt(oldIndex);
|
||||||
|
// _methods.insert(newIndex, item);
|
||||||
|
// notifyListeners();
|
||||||
|
// }
|
||||||
|
|
||||||
|
PaymentMethod? get main => methods.firstWhereOrNull((m) => m.isMain);
|
||||||
|
|
||||||
|
Future<void> updateMethod(PaymentMethod method) async => update(method.toDTO().toJson());
|
||||||
|
|
||||||
|
Future<void> setArchivedMethod({
|
||||||
|
required PaymentMethod method,
|
||||||
|
required bool newIsArchived,
|
||||||
|
}) async => setArchived(
|
||||||
|
organizationRef: _organizations.current.id,
|
||||||
|
objectRef: method.id,
|
||||||
|
newIsArchived: newIsArchived,
|
||||||
|
cascade: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<PaymentMethod> create({
|
||||||
|
required String reacipientRef,
|
||||||
|
required PaymentMethodData data,
|
||||||
|
required String name,
|
||||||
|
}) => createObject(
|
||||||
|
_organizations.current.id,
|
||||||
|
PaymentMethod(
|
||||||
|
storable: newStorable(),
|
||||||
|
permissionBound: newPermissionBound(
|
||||||
|
organizationBound: newOrganizationBound(organizationRef: _organizations.current.id),
|
||||||
|
),
|
||||||
|
recipientRef: reacipientRef,
|
||||||
|
data: data,
|
||||||
|
describable: newDescribable(name: name),
|
||||||
|
).toDTO().toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> makeMain(PaymentMethod method) {
|
||||||
|
// TODO: create separate backend method to manage main payment method
|
||||||
|
final updates = <Future<void>>[];
|
||||||
|
final currentMain = main;
|
||||||
|
if (currentMain != null) {
|
||||||
|
updates.add(updateMethod(currentMain.copyWith(isMain: false)));
|
||||||
|
}
|
||||||
|
updates.add(updateMethod(method.copyWith(isMain: true)));
|
||||||
|
return Future.wait(updates).then((_) => null);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
frontend/pshared/lib/provider/recipient/provider.dart
Normal file
76
frontend/pshared/lib/provider/recipient/provider.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
import 'package:pshared/data/mapper/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/models/recipient/filter.dart';
|
||||||
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/models/recipient/status.dart';
|
||||||
|
import 'package:pshared/models/recipient/type.dart';
|
||||||
|
import 'package:pshared/provider/organizations.dart';
|
||||||
|
import 'package:pshared/provider/template.dart';
|
||||||
|
import 'package:pshared/service/recipient/service.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class RecipientsProvider extends GenericProvider<Recipient> {
|
||||||
|
late OrganizationsProvider _organizations;
|
||||||
|
|
||||||
|
RecipientFilter _selectedFilter = RecipientFilter.all;
|
||||||
|
String _query = '';
|
||||||
|
|
||||||
|
RecipientFilter get selectedFilter => _selectedFilter;
|
||||||
|
String get query => _query;
|
||||||
|
|
||||||
|
List<Recipient> get recipients => List<Recipient>.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt)));
|
||||||
|
|
||||||
|
RecipientsProvider() : super(service: RecipientService.basicService);
|
||||||
|
|
||||||
|
List<Recipient> get filteredRecipients {
|
||||||
|
List<Recipient> filtered = recipients.where((r) {
|
||||||
|
switch (_selectedFilter) {
|
||||||
|
case RecipientFilter.ready:
|
||||||
|
return r.status == RecipientStatus.ready;
|
||||||
|
case RecipientFilter.registered:
|
||||||
|
return r.status == RecipientStatus.registered;
|
||||||
|
case RecipientFilter.notRegistered:
|
||||||
|
return r.status == RecipientStatus.notRegistered;
|
||||||
|
case RecipientFilter.all:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (_query.isNotEmpty) {
|
||||||
|
filtered = filtered.where((r) => r.matchesQuery(_query)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFilter(RecipientFilter filter) {
|
||||||
|
_selectedFilter = filter;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setQuery(String query) {
|
||||||
|
_query = query.trim().toLowerCase();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Recipient> create({
|
||||||
|
required String name,
|
||||||
|
required String email,
|
||||||
|
}) async => createObject(
|
||||||
|
_organizations.current.id,
|
||||||
|
newRecipient(
|
||||||
|
organizationRef: _organizations.current.id,
|
||||||
|
email: email,
|
||||||
|
name: name,
|
||||||
|
status: RecipientStatus.ready,
|
||||||
|
type: RecipientType.internal,
|
||||||
|
).toDTO().toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
void updateProviders(OrganizationsProvider organizations) {
|
||||||
|
_organizations = organizations;
|
||||||
|
if (_organizations.isOrganizationSet) {
|
||||||
|
load(_organizations.current.id, _organizations.current.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,21 +32,23 @@ List<T> mergeLists<T>({
|
|||||||
/// to manage state (loading, error, data) without re‑implementing service logic.
|
/// to manage state (loading, error, data) without re‑implementing service logic.
|
||||||
class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier {
|
class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier {
|
||||||
final BasicService<T> service;
|
final BasicService<T> service;
|
||||||
|
bool _isLoaded = false;
|
||||||
|
|
||||||
Resource<List<T>> _resource = Resource(data: []);
|
Resource<List<T>> _resource = Resource(data: []);
|
||||||
Resource<List<T>> get resource => _resource;
|
Resource<List<T>> get resource => _resource;
|
||||||
|
|
||||||
List<T> get items => List.unmodifiable(_resource.data ?? []);
|
List<T> get items => List.unmodifiable(_resource.data ?? []);
|
||||||
bool get isLoading => _resource.isLoading;
|
bool get isLoading => _resource.isLoading;
|
||||||
bool get isEmpty => items.isEmpty;
|
|
||||||
Object? get error => _resource.error;
|
Object? get error => _resource.error;
|
||||||
|
bool get isReady => (error == null) && _isLoaded;
|
||||||
|
|
||||||
|
bool get isCurrentSet => _currentObjectRef != null;
|
||||||
String? _currentObjectRef; // Stores the currently selected project ref
|
String? _currentObjectRef; // Stores the currently selected project ref
|
||||||
T? get currentObject => _resource.data?.firstWhereOrNull(
|
T? get currentObject => _resource.data?.firstWhereOrNull(
|
||||||
(object) => object.id == _currentObjectRef,
|
(object) => object.id == _currentObjectRef,
|
||||||
);
|
);
|
||||||
|
|
||||||
T? getItemById(String id) => items.firstWhereOrNull((item) => item.id == id);
|
T? getItemByRef(String id) => items.firstWhereOrNull((item) => item.id == id);
|
||||||
|
|
||||||
GenericProvider({required this.service});
|
GenericProvider({required this.service});
|
||||||
|
|
||||||
@@ -67,11 +69,13 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadFuture(Future<List<T>> future) async {
|
Future<List<T>> loadFuture(Future<List<T>> future) async {
|
||||||
_setResource(_resource.copyWith(isLoading: true));
|
_setResource(_resource.copyWith(isLoading: true));
|
||||||
try {
|
try {
|
||||||
final list = await future;
|
final list = await future;
|
||||||
|
_isLoaded = true;
|
||||||
_setResource(Resource(data: list, isLoading: false));
|
_setResource(Resource(data: list, isLoading: false));
|
||||||
|
return list;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_setResource(
|
_setResource(
|
||||||
_resource.copyWith(isLoading: false, error: toException(e)),
|
_resource.copyWith(isLoading: false, error: toException(e)),
|
||||||
@@ -80,17 +84,30 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> load(String organizationRef, String? parentRef) async {
|
Future<void> load(
|
||||||
|
String organizationRef,
|
||||||
|
String? parentRef, {
|
||||||
|
int? limit,
|
||||||
|
int? offset,
|
||||||
|
bool? Function()? fetchArchived,
|
||||||
|
}) async {
|
||||||
if (parentRef != null) {
|
if (parentRef != null) {
|
||||||
return loadFuture(service.list(organizationRef, parentRef));
|
await loadFuture(
|
||||||
|
service.list(
|
||||||
|
organizationRef,
|
||||||
|
parentRef,
|
||||||
|
limit: limit,
|
||||||
|
offset: offset,
|
||||||
|
fetchArchived: fetchArchived == null ? null : fetchArchived(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadItem(String itemRef) async {
|
Future<void> loadItem(String itemRef) async {
|
||||||
return loadFuture((() async => [await service.get(itemRef)])());
|
await loadFuture((() async => [await service.get(itemRef)])());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
List<T> merge(List<T> rhs) => mergeLists<T>(
|
List<T> merge(List<T> rhs) => mergeLists<T>(
|
||||||
lhs: items,
|
lhs: items,
|
||||||
rhs: rhs,
|
rhs: rhs,
|
||||||
@@ -134,11 +151,47 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete(String objectRef) async {
|
Future<void> delete(String objectRef, {Map<String, dynamic>? request}) async {
|
||||||
_setResource(_resource.copyWith(isLoading: true));
|
_setResource(_resource.copyWith(isLoading: true));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service.delete(objectRef);
|
await service.delete(objectRef, request: request);
|
||||||
|
if (_currentObjectRef == objectRef) {
|
||||||
|
_currentObjectRef = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setResource(Resource(
|
||||||
|
data: _resource.data?.where((p) => p.id != objectRef).toList(),
|
||||||
|
isLoading: false,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
_setResource(Resource(data: _resource.data, isLoading: false, error: toException(e)));
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleArchived(T item, bool currentState, {bool? cascade}) => setArchived(
|
||||||
|
organizationRef: item.organizationRef,
|
||||||
|
objectRef: item.id,
|
||||||
|
newIsArchived: !currentState,
|
||||||
|
cascade: cascade ?? true,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> setArchived({
|
||||||
|
required String organizationRef,
|
||||||
|
required String objectRef,
|
||||||
|
required bool newIsArchived,
|
||||||
|
bool? cascade,
|
||||||
|
}) async {
|
||||||
|
_setResource(_resource.copyWith(isLoading: true));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.archive(
|
||||||
|
organizationRef: organizationRef,
|
||||||
|
objectRef: objectRef,
|
||||||
|
newIsArchived: newIsArchived,
|
||||||
|
cascade: cascade,
|
||||||
|
);
|
||||||
if (_currentObjectRef == objectRef) {
|
if (_currentObjectRef == objectRef) {
|
||||||
_currentObjectRef = null;
|
_currentObjectRef = null;
|
||||||
}
|
}
|
||||||
@@ -154,11 +207,17 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool setCurrentObject(String? objectRef) {
|
bool setCurrentObject(String? objectRef) {
|
||||||
|
if (_currentObjectRef == objectRef) {
|
||||||
|
// No change, skip notification
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (objectRef == null) {
|
if (objectRef == null) {
|
||||||
_currentObjectRef = null;
|
_currentObjectRef = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_resource.data?.any((p) => p.id == objectRef) ?? false) {
|
if (_resource.data?.any((p) => p.id == objectRef) ?? false) {
|
||||||
_currentObjectRef = objectRef;
|
_currentObjectRef = objectRef;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -167,4 +226,5 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
|||||||
|
|
||||||
return false; // Object not found
|
return false; // Object not found
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
37
frontend/pshared/lib/service/recipient/pmethods.dart
Normal file
37
frontend/pshared/lib/service/recipient/pmethods.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:pshared/api/responses/payment_method.dart';
|
||||||
|
import 'package:pshared/data/mapper/payment/method.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/type.dart';
|
||||||
|
import 'package:pshared/service/services.dart';
|
||||||
|
import 'package:pshared/service/template.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentMethodService {
|
||||||
|
static const String _objectType = Services.paymentMethods;
|
||||||
|
|
||||||
|
static final BasicService<PaymentMethod> _basicService = BasicService<PaymentMethod>(
|
||||||
|
objectType: _objectType,
|
||||||
|
fromJson: (json) => PaymentMethodResponse.fromJson(json).paymentMethods.map((dto) => dto.toDomain()).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static BasicService<PaymentMethod> get basicService => _basicService;
|
||||||
|
|
||||||
|
static Future<List<PaymentMethod>> list(String organizationRef, String recipientRef) async {
|
||||||
|
return _basicService.list(organizationRef, recipientRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<PaymentMethod> get(String recipientRef) async {
|
||||||
|
return _basicService.get(recipientRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<PaymentMethod>> create(String organizationRef, PaymentMethod paymentMethod) async {
|
||||||
|
return _basicService.create(organizationRef, paymentMethod.toDTO().toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<PaymentMethod>> update(PaymentMethod paymentMethod) async {
|
||||||
|
return _basicService.update(paymentMethod.toDTO().toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<PaymentMethod>> delete(PaymentMethod paymentMethod) async {
|
||||||
|
return _basicService.delete(paymentMethod.storable.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
frontend/pshared/lib/service/recipient/service.dart
Normal file
37
frontend/pshared/lib/service/recipient/service.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:pshared/api/responses/recipient.dart';
|
||||||
|
import 'package:pshared/data/mapper/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/service/services.dart';
|
||||||
|
import 'package:pshared/service/template.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class RecipientService {
|
||||||
|
static const String _objectType = Services.recipients;
|
||||||
|
|
||||||
|
static final BasicService<Recipient> _basicService = BasicService<Recipient>(
|
||||||
|
objectType: _objectType,
|
||||||
|
fromJson: (json) => RecipientResponse.fromJson(json).recipients.map((dto) => dto.toDomain()).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static BasicService<Recipient> get basicService => _basicService;
|
||||||
|
|
||||||
|
static Future<List<Recipient>> list(String organizationRef, String _) async {
|
||||||
|
return _basicService.list(organizationRef, organizationRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Recipient> get(String recipientRef) async {
|
||||||
|
return _basicService.get(recipientRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Recipient>> create(String organizationRef, Recipient recipient) async {
|
||||||
|
return _basicService.create(organizationRef, recipient.toDTO().toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Recipient>> update(Recipient recipient) async {
|
||||||
|
return _basicService.update(recipient.toDTO().toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Recipient>> delete(Recipient recipient) async {
|
||||||
|
return _basicService.delete(recipient.storable.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'package:pshared/service/authorization/service.dart';
|
import 'package:pshared/service/authorization/service.dart';
|
||||||
|
import 'package:pshared/utils/http/params.dart';
|
||||||
|
|
||||||
|
|
||||||
class BasicService<T> {
|
class BasicService<T> {
|
||||||
@@ -15,15 +16,26 @@ class BasicService<T> {
|
|||||||
required this.fromJson,
|
required this.fromJson,
|
||||||
}) : _objectType = objectType, _logger = Logger('service.$objectType');
|
}) : _objectType = objectType, _logger = Logger('service.$objectType');
|
||||||
|
|
||||||
Future<List<T>> list(String organizationRef, String parentRef) async {
|
String _refLog(String ref) => ref.isEmpty ? '<not set>' : ref;
|
||||||
_logger.fine('Loading all objects');
|
|
||||||
|
Future<List<T>> list(String organizationRef, String parentRef, {int? limit, int? offset, bool? fetchArchived}) async {
|
||||||
|
_logger.fine('Loading all for organization ${_refLog(organizationRef)} and parent ${_refLog(organizationRef)} with: limit=${_formatParameter(limit)}, offset=${_formatParameter(offset)}, fetchArchived=${_formatParameter(fetchArchived)}...');
|
||||||
|
|
||||||
return _getObjects(
|
return _getObjects(
|
||||||
AuthorizationService.getGETResponse(_objectType, '/list/$organizationRef/$parentRef'),
|
AuthorizationService.getGETResponse(
|
||||||
|
_objectType,
|
||||||
|
paramsToUriString(
|
||||||
|
path: '/list/$organizationRef/$parentRef',
|
||||||
|
limit: limit,
|
||||||
|
offset: offset,
|
||||||
|
fetchArchived: fetchArchived,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> get(String objectRef) async {
|
Future<T> get(String objectRef) async {
|
||||||
_logger.fine('Loading object $objectRef');
|
_logger.fine('Loading $_objectType $objectRef');
|
||||||
final objects = await _getObjects(
|
final objects = await _getObjects(
|
||||||
AuthorizationService.getGETResponse(_objectType, '/$objectRef'),
|
AuthorizationService.getGETResponse(_objectType, '/$objectRef'),
|
||||||
);
|
);
|
||||||
@@ -31,24 +43,36 @@ class BasicService<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<T>> create(String organizationRef, Map<String, dynamic> request) async {
|
Future<List<T>> create(String organizationRef, Map<String, dynamic> request) async {
|
||||||
_logger.fine('Creating new object...');
|
_logger.fine('Creating new...');
|
||||||
return _getObjects(
|
return _getObjects(
|
||||||
AuthorizationService.getPOSTResponse(_objectType, '/$organizationRef', request),
|
AuthorizationService.getPOSTResponse(_objectType, '/$organizationRef', request),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<T>> update(Map<String, dynamic> request) async {
|
Future<List<T>> update(Map<String, dynamic> request) async {
|
||||||
_logger.fine('Patching object...');
|
_logger.fine('Patching...');
|
||||||
return _getObjects(
|
return _getObjects(
|
||||||
AuthorizationService.getPUTResponse(_objectType, '/', request,
|
AuthorizationService.getPUTResponse(_objectType, '/', request,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<T>> delete(String objecRef) async {
|
Future<List<T>> delete(String objecRef, {Map<String, dynamic>? request}) async {
|
||||||
_logger.fine('Deleting object $objecRef');
|
_logger.fine('Deleting $_objectType $objecRef');
|
||||||
return _getObjects(
|
return _getObjects(
|
||||||
AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', {}),
|
AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', request ?? {}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<T>> archive({
|
||||||
|
required String organizationRef,
|
||||||
|
required String objectRef,
|
||||||
|
required bool newIsArchived,
|
||||||
|
bool? cascade,
|
||||||
|
}) async {
|
||||||
|
_logger.fine('Setting new archive status $newIsArchived to $objectRef');
|
||||||
|
return _getObjects(
|
||||||
|
AuthorizationService.getGETResponse(_objectType, '/archive/$organizationRef/$objectRef?archived=$newIsArchived&cascade=${cascade ?? false}'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +83,12 @@ class BasicService<T> {
|
|||||||
_logger.fine('Fetched ${objects.length} object(s)');
|
_logger.fine('Fetched ${objects.length} object(s)');
|
||||||
return objects;
|
return objects;
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_logger.severe('Failed to fetch objects', e, stackTrace);
|
_logger.severe('Failed to fetch', e, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatParameter(dynamic value) {
|
||||||
|
return value?.toString() ?? '<not specified>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
frontend/pshared/lib/utils/http/params.dart
Normal file
38
frontend/pshared/lib/utils/http/params.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Query parameter constants
|
||||||
|
const String _limitParam = 'limit';
|
||||||
|
const String _offsetParam = 'offset';
|
||||||
|
const String _archivedParam = 'archived';
|
||||||
|
|
||||||
|
void _addIfNotNull(Map<String, String> params, String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
params[key] = value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri paramsToUri({
|
||||||
|
required String path,
|
||||||
|
int? limit,
|
||||||
|
int? offset,
|
||||||
|
bool? fetchArchived,
|
||||||
|
Map<String, String>? params,
|
||||||
|
}) {
|
||||||
|
Map<String, String> queryParams = params ?? {};
|
||||||
|
_addIfNotNull(queryParams, _limitParam, limit);
|
||||||
|
_addIfNotNull(queryParams, _offsetParam, offset);
|
||||||
|
_addIfNotNull(queryParams, _archivedParam, fetchArchived);
|
||||||
|
|
||||||
|
// Build URL with query parameters using Uri class
|
||||||
|
return Uri(
|
||||||
|
path: path,
|
||||||
|
queryParameters: queryParams.isEmpty ? null : queryParams,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String paramsToUriString({
|
||||||
|
required String path,
|
||||||
|
Map<String, String> queryParams = const {},
|
||||||
|
int? limit,
|
||||||
|
int? offset,
|
||||||
|
bool? fetchArchived,
|
||||||
|
}) => paramsToUri(path: path, limit: limit, offset: offset, fetchArchived: fetchArchived).toString();
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/generated/i18n/ps_localizations.dart';
|
||||||
import 'package:pshared/provider/template.dart';
|
import 'package:pshared/provider/template.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -20,9 +21,10 @@ class ResourceContainer<T extends GenericProvider> extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Consumer<T>(builder: (context, provider, _) {
|
Widget build(BuildContext context) => Consumer<T>(builder: (context, provider, _) {
|
||||||
if (provider.isLoading) return loading ?? Center(child: CircularProgressIndicator());
|
final l10n = PSLocalizations.of(context)!;
|
||||||
if (provider.error != null) return error ?? Text('Error while loading data. Try again');
|
if (provider.isLoading) return loading ?? const Center(child: CircularProgressIndicator());
|
||||||
if (provider.isEmpty) return empty ?? Text('Empty data');
|
if (provider.error != null) return error ?? Text(l10n.resourceLoadError);
|
||||||
|
if (provider.items.isEmpty) return empty ?? Text(l10n.resourceEmpty);
|
||||||
return builder(context, provider);
|
return builder(context, provider);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
handle @monetixSuccess {
|
handle @monetixSuccess {
|
||||||
rewrite * /monetix/callback
|
rewrite * /monetix/callback
|
||||||
reverse_proxy sendico_mntx_gateway:8080
|
reverse_proxy sendico_mntx_gateway:8084
|
||||||
header Cache-Control "no-cache, no-store, must-revalidate"
|
header Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
handle @monetixFail {
|
handle @monetixFail {
|
||||||
rewrite * /monetix/callback
|
rewrite * /monetix/callback
|
||||||
reverse_proxy sendico_mntx_gateway:8080
|
reverse_proxy sendico_mntx_gateway:8084
|
||||||
header Cache-Control "no-cache, no-store, must-revalidate"
|
header Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class PayApp extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => MaterialApp.router(
|
Widget build(BuildContext context) => MaterialApp.router(
|
||||||
title: 'sendico',
|
title: 'Sendico',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Constants.themeColor),
|
colorScheme: ColorScheme.fromSeed(seedColor: Constants.themeColor),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
|||||||
112
frontend/pweb/lib/app/router/payout_routes.dart
Normal file
112
frontend/pweb/lib/app/router/payout_routes.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class PayoutRoutes {
|
||||||
|
static const dashboard = 'dashboard';
|
||||||
|
static const sendPayout = payment;
|
||||||
|
static const recipients = 'payout-recipients';
|
||||||
|
static const addRecipient = 'payout-add-recipient';
|
||||||
|
static const payment = 'payout-payment';
|
||||||
|
static const settings = 'payout-settings';
|
||||||
|
static const reports = 'payout-reports';
|
||||||
|
static const methods = 'payout-methods';
|
||||||
|
static const editWallet = 'payout-edit-wallet';
|
||||||
|
static const walletTopUp = 'payout-wallet-top-up';
|
||||||
|
|
||||||
|
static const dashboardPath = '/dashboard';
|
||||||
|
static const recipientsPath = '/dashboard/recipients';
|
||||||
|
static const addRecipientPath = '/dashboard/recipients/add';
|
||||||
|
static const paymentPath = '/dashboard/payment';
|
||||||
|
static const settingsPath = '/dashboard/settings';
|
||||||
|
static const reportsPath = '/dashboard/reports';
|
||||||
|
static const methodsPath = '/dashboard/methods';
|
||||||
|
static const editWalletPath = '/dashboard/methods/edit';
|
||||||
|
static const walletTopUpPath = '/dashboard/wallet/top-up';
|
||||||
|
|
||||||
|
static String nameFor(PayoutDestination destination) {
|
||||||
|
switch (destination) {
|
||||||
|
case PayoutDestination.dashboard:
|
||||||
|
return dashboard;
|
||||||
|
case PayoutDestination.sendPayout:
|
||||||
|
return payment;
|
||||||
|
case PayoutDestination.recipients:
|
||||||
|
return recipients;
|
||||||
|
case PayoutDestination.addrecipient:
|
||||||
|
return addRecipient;
|
||||||
|
case PayoutDestination.payment:
|
||||||
|
return payment;
|
||||||
|
case PayoutDestination.settings:
|
||||||
|
return settings;
|
||||||
|
case PayoutDestination.reports:
|
||||||
|
return reports;
|
||||||
|
case PayoutDestination.methods:
|
||||||
|
return methods;
|
||||||
|
case PayoutDestination.editwallet:
|
||||||
|
return editWallet;
|
||||||
|
case PayoutDestination.walletTopUp:
|
||||||
|
return walletTopUp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String pathFor(PayoutDestination destination) {
|
||||||
|
switch (destination) {
|
||||||
|
case PayoutDestination.dashboard:
|
||||||
|
return dashboardPath;
|
||||||
|
case PayoutDestination.sendPayout:
|
||||||
|
return paymentPath;
|
||||||
|
case PayoutDestination.recipients:
|
||||||
|
return recipientsPath;
|
||||||
|
case PayoutDestination.addrecipient:
|
||||||
|
return addRecipientPath;
|
||||||
|
case PayoutDestination.payment:
|
||||||
|
return paymentPath;
|
||||||
|
case PayoutDestination.settings:
|
||||||
|
return settingsPath;
|
||||||
|
case PayoutDestination.reports:
|
||||||
|
return reportsPath;
|
||||||
|
case PayoutDestination.methods:
|
||||||
|
return methodsPath;
|
||||||
|
case PayoutDestination.editwallet:
|
||||||
|
return editWalletPath;
|
||||||
|
case PayoutDestination.walletTopUp:
|
||||||
|
return walletTopUpPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static PayoutDestination? destinationFor(String? routeName) {
|
||||||
|
switch (routeName) {
|
||||||
|
case dashboard:
|
||||||
|
return PayoutDestination.dashboard;
|
||||||
|
case sendPayout:
|
||||||
|
return PayoutDestination.payment;
|
||||||
|
case recipients:
|
||||||
|
return PayoutDestination.recipients;
|
||||||
|
case addRecipient:
|
||||||
|
return PayoutDestination.addrecipient;
|
||||||
|
case payment:
|
||||||
|
return PayoutDestination.payment;
|
||||||
|
case settings:
|
||||||
|
return PayoutDestination.settings;
|
||||||
|
case reports:
|
||||||
|
return PayoutDestination.reports;
|
||||||
|
case methods:
|
||||||
|
return PayoutDestination.methods;
|
||||||
|
case editWallet:
|
||||||
|
return PayoutDestination.editwallet;
|
||||||
|
case walletTopUp:
|
||||||
|
return PayoutDestination.walletTopUp;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PayoutNavigation on BuildContext {
|
||||||
|
void goToPayout(PayoutDestination destination) => goNamed(PayoutRoutes.nameFor(destination));
|
||||||
|
|
||||||
|
void pushToPayout(PayoutDestination destination) => pushNamed(PayoutRoutes.nameFor(destination));
|
||||||
|
}
|
||||||
160
frontend/pweb/lib/app/router/payout_shell.dart
Normal file
160
frontend/pweb/lib/app/router/payout_shell.dart
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/app/router/pages.dart';
|
||||||
|
import 'package:pweb/app/router/payout_routes.dart';
|
||||||
|
import 'package:pweb/pages/address_book/form/page.dart';
|
||||||
|
import 'package:pweb/pages/address_book/page/page.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/dashboard.dart';
|
||||||
|
import 'package:pweb/pages/payment_methods/page.dart';
|
||||||
|
import 'package:pweb/pages/payout_page/page.dart';
|
||||||
|
import 'package:pweb/pages/payout_page/wallet/edit/page.dart';
|
||||||
|
import 'package:pweb/pages/report/page.dart';
|
||||||
|
import 'package:pweb/pages/settings/profile/page.dart';
|
||||||
|
import 'package:pweb/pages/wallet_top_up/page.dart';
|
||||||
|
import 'package:pweb/providers/page_selector.dart';
|
||||||
|
import 'package:pweb/widgets/error/snackbar.dart';
|
||||||
|
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||||
|
import 'package:pweb/widgets/sidebar/page.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
RouteBase payoutShellRoute() => ShellRoute(
|
||||||
|
builder: (context, state, child) => PageSelector(
|
||||||
|
child: child,
|
||||||
|
routerState: state,
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
name: PayoutRoutes.dashboard,
|
||||||
|
path: routerPage(Pages.dashboard),
|
||||||
|
pageBuilder: (context, _) => NoTransitionPage(
|
||||||
|
child: DashboardPage(
|
||||||
|
onRecipientSelected: (recipient) => context
|
||||||
|
.read<PageSelectorProvider>()
|
||||||
|
.selectRecipient(context, recipient),
|
||||||
|
onGoToPaymentWithoutRecipient: (type) => context
|
||||||
|
.read<PageSelectorProvider>()
|
||||||
|
.startPaymentWithoutRecipient(context, type),
|
||||||
|
onTopUp: (wallet) => context
|
||||||
|
.read<PageSelectorProvider>()
|
||||||
|
.openWalletTopUp(context, wallet),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: PayoutRoutes.recipients,
|
||||||
|
path: PayoutRoutes.recipientsPath,
|
||||||
|
pageBuilder: (context, _) {
|
||||||
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
return NoTransitionPage(
|
||||||
|
child: RecipientAddressBookPage(
|
||||||
|
onRecipientSelected: (recipient) => context
|
||||||
|
.read<PageSelectorProvider>()
|
||||||
|
.selectRecipient(context, recipient, fromList: true),
|
||||||
|
onAddRecipient: () => context
|
||||||
|
.read<PageSelectorProvider>()
|
||||||
|
.goToAddRecipient(context),
|
||||||
|
onEditRecipient: (recipient) => context
|
||||||
|
.read<PageSelectorProvider>()
|
||||||
|
.editRecipient(context, recipient, fromList: true),
|
||||||
|
onDeleteRecipient: (recipient) => executeActionWithNotification(
|
||||||
|
context: context,
|
||||||
|
action: () async =>
|
||||||
|
context.read<RecipientsProvider>().delete(recipient.id),
|
||||||
|
successMessage: loc.recipientDeletedSuccessfully,
|
||||||
|
errorMessage: loc.errorDeleteRecipient,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: PayoutRoutes.addRecipient,
|
||||||
|
path: PayoutRoutes.addRecipientPath,
|
||||||
|
pageBuilder: (context, _) {
|
||||||
|
final selector = context.read<PageSelectorProvider>();
|
||||||
|
final recipient = selector.recipientProvider.currentObject;
|
||||||
|
return NoTransitionPage(
|
||||||
|
child: AdressBookRecipientForm(
|
||||||
|
recipient: recipient,
|
||||||
|
onSaved: (_) => selector.selectPage(
|
||||||
|
context,
|
||||||
|
PayoutDestination.recipients,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: PayoutRoutes.payment,
|
||||||
|
path: PayoutRoutes.paymentPath,
|
||||||
|
pageBuilder: (context, _) => NoTransitionPage(
|
||||||
|
child: PaymentPage(
|
||||||
|
onBack: (_) => context
|
||||||
|
.read<PageSelectorProvider>()
|
||||||
|
.goBackFromPayment(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: PayoutRoutes.settings,
|
||||||
|
path: PayoutRoutes.settingsPath,
|
||||||
|
pageBuilder: (_, __) => const NoTransitionPage(
|
||||||
|
child: ProfileSettingsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: PayoutRoutes.reports,
|
||||||
|
path: PayoutRoutes.reportsPath,
|
||||||
|
pageBuilder: (_, __) => const NoTransitionPage(
|
||||||
|
child: OperationHistoryPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: PayoutRoutes.methods,
|
||||||
|
path: PayoutRoutes.methodsPath,
|
||||||
|
pageBuilder: (context, _) => NoTransitionPage(
|
||||||
|
child: PaymentConfigPage(
|
||||||
|
onWalletTap: (wallet) => context
|
||||||
|
.read<PageSelectorProvider>()
|
||||||
|
.selectWallet(context, wallet),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: PayoutRoutes.editWallet,
|
||||||
|
path: PayoutRoutes.editWalletPath,
|
||||||
|
pageBuilder: (context, _) {
|
||||||
|
final provider = context.read<PageSelectorProvider>();
|
||||||
|
final wallet = provider.walletsProvider.selectedWallet;
|
||||||
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
return NoTransitionPage(
|
||||||
|
child: wallet != null
|
||||||
|
? WalletEditPage(
|
||||||
|
onBack: () => provider.goBackFromWalletEdit(context),
|
||||||
|
)
|
||||||
|
: Center(child: Text(loc.noWalletSelected)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: PayoutRoutes.walletTopUp,
|
||||||
|
path: PayoutRoutes.walletTopUpPath,
|
||||||
|
pageBuilder: (context, _) => NoTransitionPage(
|
||||||
|
child: WalletTopUpPage(
|
||||||
|
onBack: () => context
|
||||||
|
.read<PageSelectorProvider>()
|
||||||
|
.goBackFromWalletTopUp(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import 'package:pweb/app/router/pages.dart';
|
|
||||||
import 'package:pweb/app/router/page_params.dart';
|
import 'package:pweb/app/router/page_params.dart';
|
||||||
|
import 'package:pweb/app/router/pages.dart';
|
||||||
|
import 'package:pweb/app/router/payout_shell.dart';
|
||||||
|
import 'package:pweb/app/router/payout_routes.dart';
|
||||||
import 'package:pweb/pages/2fa/page.dart';
|
import 'package:pweb/pages/2fa/page.dart';
|
||||||
|
import 'package:pweb/pages/errors/not_found.dart';
|
||||||
|
import 'package:pweb/pages/login/page.dart';
|
||||||
import 'package:pweb/pages/signup/page.dart';
|
import 'package:pweb/pages/signup/page.dart';
|
||||||
import 'package:pweb/pages/verification/page.dart';
|
import 'package:pweb/pages/verification/page.dart';
|
||||||
import 'package:pweb/widgets/sidebar/page.dart';
|
|
||||||
import 'package:pweb/pages/login/page.dart';
|
|
||||||
import 'package:pweb/pages/errors/not_found.dart';
|
|
||||||
|
|
||||||
|
|
||||||
GoRouter createRouter() => GoRouter(
|
GoRouter createRouter() => GoRouter(
|
||||||
@@ -16,40 +17,33 @@ GoRouter createRouter() => GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
name: Pages.root.name,
|
name: Pages.root.name,
|
||||||
path: routerPage(Pages.root),
|
path: routerPage(Pages.root),
|
||||||
builder: (_, _) => const LoginPage(),
|
builder: (_, __) => const LoginPage(),
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
name: Pages.login.name,
|
|
||||||
path: routerPage(Pages.login),
|
|
||||||
builder: (_, _) => const LoginPage(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: Pages.dashboard.name,
|
|
||||||
path: routerPage(Pages.dashboard),
|
|
||||||
builder: (_, _) => const PageSelector(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: Pages.sfactor.name,
|
|
||||||
path: routerPage(Pages.sfactor),
|
|
||||||
builder: (context, _) => TwoFactorCodePage(
|
|
||||||
onVerificationSuccess: () {
|
|
||||||
// trigger organization load
|
|
||||||
context.goNamed(Pages.dashboard.name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: Pages.signup.name,
|
|
||||||
path: routerPage(Pages.signup),
|
|
||||||
builder: (_, _) => const SignUpPage(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: Pages.verify.name,
|
|
||||||
path: '${routerPage(Pages.verify)}${routerAddParam(PageParams.token)}',
|
|
||||||
builder: (_, state) => AccountVerificationPage(token: state.pathParameters[PageParams.token.name]!),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: Pages.login.name,
|
||||||
|
path: routerPage(Pages.login),
|
||||||
|
builder: (_, __) => const LoginPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: Pages.sfactor.name,
|
||||||
|
path: routerPage(Pages.sfactor),
|
||||||
|
builder: (context, _) => TwoFactorCodePage(
|
||||||
|
onVerificationSuccess: () => context.goNamed(PayoutRoutes.dashboard),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: Pages.signup.name,
|
||||||
|
path: routerPage(Pages.signup),
|
||||||
|
builder: (_, __) => const SignUpPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: Pages.verify.name,
|
||||||
|
path: '${routerPage(Pages.verify)}${routerAddParam(PageParams.token)}',
|
||||||
|
builder: (_, state) => AccountVerificationPage(
|
||||||
|
token: state.pathParameters[PageParams.token.name]!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
payoutShellRoute(),
|
||||||
],
|
],
|
||||||
errorBuilder: (_, _) => const NotFoundPage(),
|
errorBuilder: (_, __) => const NotFoundPage(),
|
||||||
);
|
);
|
||||||
@@ -21,6 +21,10 @@ extension WalletUiMapper on domain.WalletModel {
|
|||||||
currency: currency,
|
currency: currency,
|
||||||
isHidden: true,
|
isHidden: true,
|
||||||
calculatedAt: balance?.calculatedAt ?? DateTime.now(),
|
calculatedAt: balance?.calculatedAt ?? DateTime.now(),
|
||||||
|
depositAddress: depositAddress,
|
||||||
|
network: asset.chain,
|
||||||
|
tokenSymbol: asset.tokenSymbol,
|
||||||
|
contractAddress: asset.contractAddress,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,6 +311,7 @@
|
|||||||
"paymentTypeBankAccount": "Russian Bank Account",
|
"paymentTypeBankAccount": "Russian Bank Account",
|
||||||
"paymentTypeIban": "IBAN",
|
"paymentTypeIban": "IBAN",
|
||||||
"paymentTypeWallet": "Wallet",
|
"paymentTypeWallet": "Wallet",
|
||||||
|
"paymentTypeCryptoAddress": "Crypto address",
|
||||||
|
|
||||||
"cardNumber": "Card Number",
|
"cardNumber": "Card Number",
|
||||||
"enterCardNumber": "Enter the card number",
|
"enterCardNumber": "Enter the card number",
|
||||||
@@ -428,6 +429,19 @@
|
|||||||
"companyName": "Name of your company",
|
"companyName": "Name of your company",
|
||||||
"companynameRequired": "Company name required",
|
"companynameRequired": "Company name required",
|
||||||
|
|
||||||
|
"errorSaveRecipient": "Failed to save recipient",
|
||||||
|
"@errorSaveRecipient": {
|
||||||
|
"description": "Error message displayed when saving a recipient fails"
|
||||||
|
},
|
||||||
|
"recipientDeletedSuccessfully": "Recipient deleted successfully",
|
||||||
|
"@recipientDeletedSuccessfully": {
|
||||||
|
"description": "Success message displayed when a recipient is deleted"
|
||||||
|
},
|
||||||
|
"errorDeleteRecipient": "Failed to delete recipient",
|
||||||
|
"@errorDeleteRecipient": {
|
||||||
|
"description": "Error message displayed when deleting a recipient fails"
|
||||||
|
},
|
||||||
|
|
||||||
"errorSignUp": "Error occured while signing up, try again later",
|
"errorSignUp": "Error occured while signing up, try again later",
|
||||||
"companyDescription": "Company Description",
|
"companyDescription": "Company Description",
|
||||||
"companyDescriptionHint": "Describe any of the fields of the Company's business",
|
"companyDescriptionHint": "Describe any of the fields of the Company's business",
|
||||||
@@ -455,6 +469,17 @@
|
|||||||
"walletNameUpdateFailed": "Failed to update wallet name",
|
"walletNameUpdateFailed": "Failed to update wallet name",
|
||||||
"walletNameSaved": "Wallet name saved",
|
"walletNameSaved": "Wallet name saved",
|
||||||
"topUpBalance": "Top Up Balance",
|
"topUpBalance": "Top Up Balance",
|
||||||
|
"walletTopUpTitle": "Add funds to wallet",
|
||||||
|
"walletTopUpDetailsTitle": "Funding details",
|
||||||
|
"walletTopUpDescription": "Send funds to this address to increase your wallet balance.",
|
||||||
|
"walletTopUpAssetLabel": "Asset",
|
||||||
|
"walletTopUpNetworkLabel": "Network",
|
||||||
|
"walletTopUpAddressLabel": "Deposit address",
|
||||||
|
"walletTopUpQrLabel": "QR code for deposit",
|
||||||
|
"walletTopUpHint": "Only send funds on the specified network. Deposits may take a few minutes to confirm.",
|
||||||
|
"walletTopUpUnavailable": "Top-up details are unavailable for this wallet yet.",
|
||||||
|
"copyAddress": "Copy address",
|
||||||
|
"addressCopied": "Address copied",
|
||||||
"addFunctionality": "Add functionality",
|
"addFunctionality": "Add functionality",
|
||||||
"walletHistoryEmpty": "No history yet",
|
"walletHistoryEmpty": "No history yet",
|
||||||
"colType": "Type",
|
"colType": "Type",
|
||||||
|
|||||||
@@ -311,6 +311,7 @@
|
|||||||
"paymentTypeBankAccount": "Российский банковский счет",
|
"paymentTypeBankAccount": "Российский банковский счет",
|
||||||
"paymentTypeIban": "IBAN",
|
"paymentTypeIban": "IBAN",
|
||||||
"paymentTypeWallet": "Кошелек",
|
"paymentTypeWallet": "Кошелек",
|
||||||
|
"paymentTypeCryptoAddress": "Крипто-адрес",
|
||||||
|
|
||||||
"cardNumber": "Номер карты",
|
"cardNumber": "Номер карты",
|
||||||
"enterCardNumber": "Введите номер карты",
|
"enterCardNumber": "Введите номер карты",
|
||||||
@@ -428,6 +429,19 @@
|
|||||||
"companyName": "Название вашей компании",
|
"companyName": "Название вашей компании",
|
||||||
"companynameRequired": "Необходимо указать название компании",
|
"companynameRequired": "Необходимо указать название компании",
|
||||||
|
|
||||||
|
"errorSaveRecipient": "Не удалось сохранить получателя",
|
||||||
|
"@errorSaveRecipient": {
|
||||||
|
"description": "Сообщение об ошибке при неудачном сохранении получателя"
|
||||||
|
},
|
||||||
|
"recipientDeletedSuccessfully": "Получатель успешно удален",
|
||||||
|
"@recipientDeletedSuccessfully": {
|
||||||
|
"description": "Сообщение об успешном удалении получателя"
|
||||||
|
},
|
||||||
|
"errorDeleteRecipient": "Не удалось удалить получателя",
|
||||||
|
"@errorDeleteRecipient": {
|
||||||
|
"description": "Сообщение об ошибке при неудачном удалении получателя"
|
||||||
|
},
|
||||||
|
|
||||||
"errorSignUp": "Произошла ошибка при регистрации, попробуйте позже",
|
"errorSignUp": "Произошла ошибка при регистрации, попробуйте позже",
|
||||||
"companyDescription": "Описание компании",
|
"companyDescription": "Описание компании",
|
||||||
"companyDescriptionHint": "Опишите любую из сфер деятельности компании",
|
"companyDescriptionHint": "Опишите любую из сфер деятельности компании",
|
||||||
@@ -456,6 +470,17 @@
|
|||||||
"walletNameUpdateFailed": "Не удалось обновить название кошелька",
|
"walletNameUpdateFailed": "Не удалось обновить название кошелька",
|
||||||
"walletNameSaved": "Название кошелька сохранено",
|
"walletNameSaved": "Название кошелька сохранено",
|
||||||
"topUpBalance": "Пополнить баланс",
|
"topUpBalance": "Пополнить баланс",
|
||||||
|
"walletTopUpTitle": "Пополнение кошелька",
|
||||||
|
"walletTopUpDetailsTitle": "Данные для пополнения",
|
||||||
|
"walletTopUpDescription": "Отправьте средства на этот адрес, чтобы пополнить баланс кошелька.",
|
||||||
|
"walletTopUpAssetLabel": "Актив",
|
||||||
|
"walletTopUpNetworkLabel": "Сеть",
|
||||||
|
"walletTopUpAddressLabel": "Адрес для пополнения",
|
||||||
|
"walletTopUpQrLabel": "QR-код для пополнения",
|
||||||
|
"walletTopUpHint": "Отправляйте средства только в указанной сети. Подтверждение может занять несколько минут.",
|
||||||
|
"walletTopUpUnavailable": "Данные для пополнения пока недоступны для этого кошелька.",
|
||||||
|
"copyAddress": "Скопировать адрес",
|
||||||
|
"addressCopied": "Адрес скопирован",
|
||||||
"addFunctionality": "Добавить функциональность",
|
"addFunctionality": "Добавить функциональность",
|
||||||
"walletHistoryEmpty": "История пуста",
|
"walletHistoryEmpty": "История пуста",
|
||||||
"colType": "Тип",
|
"colType": "Тип",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import 'package:pshared/provider/locale.dart';
|
|||||||
import 'package:pshared/provider/permissions.dart';
|
import 'package:pshared/provider/permissions.dart';
|
||||||
import 'package:pshared/provider/account.dart';
|
import 'package:pshared/provider/account.dart';
|
||||||
import 'package:pshared/provider/organizations.dart';
|
import 'package:pshared/provider/organizations.dart';
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
|
||||||
import 'package:pweb/app/app.dart';
|
import 'package:pweb/app/app.dart';
|
||||||
import 'package:pweb/app/timeago.dart';
|
import 'package:pweb/app/timeago.dart';
|
||||||
@@ -19,17 +21,13 @@ import 'package:pweb/providers/carousel.dart';
|
|||||||
import 'package:pweb/providers/mock_payment.dart';
|
import 'package:pweb/providers/mock_payment.dart';
|
||||||
import 'package:pweb/providers/operatioins.dart';
|
import 'package:pweb/providers/operatioins.dart';
|
||||||
import 'package:pweb/providers/page_selector.dart';
|
import 'package:pweb/providers/page_selector.dart';
|
||||||
import 'package:pweb/providers/payment_methods.dart';
|
|
||||||
import 'package:pweb/providers/recipient.dart';
|
|
||||||
import 'package:pweb/providers/two_factor.dart';
|
import 'package:pweb/providers/two_factor.dart';
|
||||||
import 'package:pweb/providers/upload_history.dart';
|
import 'package:pweb/providers/upload_history.dart';
|
||||||
import 'package:pweb/providers/wallets.dart';
|
import 'package:pweb/providers/wallets.dart';
|
||||||
import 'package:pweb/providers/wallet_transactions.dart';
|
import 'package:pweb/providers/wallet_transactions.dart';
|
||||||
// import 'package:pweb/services/amplitude.dart';
|
// import 'package:pweb/services/amplitude.dart';
|
||||||
import 'package:pweb/services/operations.dart';
|
import 'package:pweb/services/operations.dart';
|
||||||
import 'package:pweb/services/payments/payment_methods.dart';
|
import 'package:pweb/services/payments/history.dart';
|
||||||
import 'package:pweb/services/payments/upload_history.dart';
|
|
||||||
import 'package:pweb/services/recipient/recipient.dart';
|
|
||||||
import 'package:pweb/services/wallet_transactions.dart';
|
import 'package:pweb/services/wallet_transactions.dart';
|
||||||
import 'package:pweb/services/wallets.dart';
|
import 'package:pweb/services/wallets.dart';
|
||||||
|
|
||||||
@@ -77,8 +75,13 @@ void main() async {
|
|||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(),
|
create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(),
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProxyProvider<OrganizationsProvider, RecipientsProvider>(
|
||||||
create: (_) => PaymentMethodsProvider(service: MockPaymentMethodsService())..loadMethods(),
|
create: (_) => RecipientsProvider(),
|
||||||
|
update: (context, organizations, provider) => provider!..updateProviders(organizations),
|
||||||
|
),
|
||||||
|
ChangeNotifierProxyProvider2<OrganizationsProvider, RecipientsProvider, PaymentMethodsProvider>(
|
||||||
|
create: (_) => PaymentMethodsProvider(),
|
||||||
|
update: (context, organizations, recipients, provider) => provider!..updateProviders(organizations, recipients),
|
||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider<OrganizationsProvider, WalletsProvider>(
|
ChangeNotifierProxyProvider<OrganizationsProvider, WalletsProvider>(
|
||||||
create: (_) => WalletsProvider(ApiWalletsService()),
|
create: (_) => WalletsProvider(ApiWalletsService()),
|
||||||
@@ -90,18 +93,11 @@ void main() async {
|
|||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => MockPaymentProvider(),
|
create: (_) => MockPaymentProvider(),
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
|
||||||
create: (_) => RecipientProvider(RecipientService())..loadRecipients(),
|
|
||||||
),
|
|
||||||
|
|
||||||
ChangeNotifierProxyProvider3<RecipientProvider, WalletsProvider, PaymentMethodsProvider, PageSelectorProvider>(
|
ChangeNotifierProxyProvider3<RecipientsProvider, WalletsProvider, PaymentMethodsProvider, PageSelectorProvider>(
|
||||||
create: (context) => PageSelectorProvider(),
|
create: (context) => PageSelectorProvider(),
|
||||||
update: (context, recipientProv, walletsProv, methodsProv, previous) =>
|
update: (context, recipientProv, walletsProv, methodsProv, previous) =>
|
||||||
previous ?? PageSelectorProvider(
|
previous ?? PageSelectorProvider()..update(recipientProv, walletsProv, methodsProv),
|
||||||
recipientProvider: recipientProv,
|
|
||||||
walletsProvider: walletsProv,
|
|
||||||
methodsProvider: methodsProv,
|
|
||||||
)..update(recipientProv, walletsProv, methodsProv),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ class Wallet {
|
|||||||
final Currency currency;
|
final Currency currency;
|
||||||
final bool isHidden;
|
final bool isHidden;
|
||||||
final DateTime calculatedAt;
|
final DateTime calculatedAt;
|
||||||
|
final String? depositAddress;
|
||||||
|
final String? network;
|
||||||
|
final String? tokenSymbol;
|
||||||
|
final String? contractAddress;
|
||||||
|
|
||||||
Wallet({
|
Wallet({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -18,6 +22,10 @@ class Wallet {
|
|||||||
required this.currency,
|
required this.currency,
|
||||||
required this.calculatedAt,
|
required this.calculatedAt,
|
||||||
this.isHidden = true,
|
this.isHidden = true,
|
||||||
|
this.depositAddress,
|
||||||
|
this.network,
|
||||||
|
this.tokenSymbol,
|
||||||
|
this.contractAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
Wallet copyWith({
|
Wallet copyWith({
|
||||||
@@ -27,6 +35,10 @@ class Wallet {
|
|||||||
Currency? currency,
|
Currency? currency,
|
||||||
String? walletUserID,
|
String? walletUserID,
|
||||||
bool? isHidden,
|
bool? isHidden,
|
||||||
|
String? depositAddress,
|
||||||
|
String? network,
|
||||||
|
String? tokenSymbol,
|
||||||
|
String? contractAddress,
|
||||||
}) => Wallet(
|
}) => Wallet(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
@@ -35,5 +47,9 @@ class Wallet {
|
|||||||
walletUserID: walletUserID ?? this.walletUserID,
|
walletUserID: walletUserID ?? this.walletUserID,
|
||||||
isHidden: isHidden ?? this.isHidden,
|
isHidden: isHidden ?? this.isHidden,
|
||||||
calculatedAt: calculatedAt,
|
calculatedAt: calculatedAt,
|
||||||
|
depositAddress: depositAddress ?? this.depositAddress,
|
||||||
|
network: network ?? this.network,
|
||||||
|
tokenSymbol: tokenSymbol ?? this.tokenSymbol,
|
||||||
|
contractAddress: contractAddress ?? this.contractAddress,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:pshared/models/payment/type.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/payment_methods/form.dart';
|
import 'package:pweb/pages/payment_methods/form.dart';
|
||||||
@@ -9,8 +10,8 @@ import 'package:pweb/pages/payment_methods/icon.dart';
|
|||||||
class AdressBookPaymentMethodTile extends StatefulWidget {
|
class AdressBookPaymentMethodTile extends StatefulWidget {
|
||||||
final PaymentType type;
|
final PaymentType type;
|
||||||
final String title;
|
final String title;
|
||||||
final Map<PaymentType, Object?> methods;
|
final MethodMap methods;
|
||||||
final ValueChanged<Object?> onChanged;
|
final ValueChanged<PaymentMethodData?> onChanged;
|
||||||
|
|
||||||
final double spacingM;
|
final double spacingM;
|
||||||
final double spacingS;
|
final double spacingS;
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/methods/card.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
|
||||||
import 'package:pshared/models/payment/methods/iban.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
|
||||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
import 'package:pshared/models/payment/methods/data.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/recipient/status.dart';
|
import 'package:pshared/models/recipient/status.dart';
|
||||||
import 'package:pshared/models/recipient/type.dart';
|
import 'package:pshared/models/recipient/type.dart';
|
||||||
|
import 'package:pshared/provider/organizations.dart';
|
||||||
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/address_book/form/view.dart';
|
import 'package:pweb/pages/address_book/form/view.dart';
|
||||||
import 'package:pweb/services/amplitude.dart';
|
// import 'package:pweb/services/amplitude.dart';
|
||||||
|
import 'package:pweb/utils/error/snackbar.dart';
|
||||||
|
import 'package:pweb/utils/payment/label.dart';
|
||||||
|
import 'package:pweb/utils/snackbar.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -32,81 +38,121 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
|
|||||||
late TextEditingController _emailCtrl;
|
late TextEditingController _emailCtrl;
|
||||||
RecipientType _type = RecipientType.internal;
|
RecipientType _type = RecipientType.internal;
|
||||||
RecipientStatus _status = RecipientStatus.ready;
|
RecipientStatus _status = RecipientStatus.ready;
|
||||||
final Map<PaymentType, Object?> _methods = {};
|
final MethodMap _methods = {};
|
||||||
|
late PaymentMethodsProvider _methodsProvider;
|
||||||
|
|
||||||
|
Future<void> _loadMethods() async {
|
||||||
|
_methodsProvider = PaymentMethodsProvider()..addListener(_onProviderChanged);
|
||||||
|
await _methodsProvider.loadMethods(
|
||||||
|
context.read<OrganizationsProvider>(),
|
||||||
|
widget.recipient?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final m in _methodsProvider.methods) {
|
||||||
|
_methods[m.type] = switch (m.type) {
|
||||||
|
PaymentType.card => m.cardData,
|
||||||
|
PaymentType.iban => m.ibanData,
|
||||||
|
PaymentType.wallet => m.walletData,
|
||||||
|
PaymentType.bankAccount => m.bankAccountData,
|
||||||
|
PaymentType.cryptoAddress => m.cryptoAddressData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final r = widget.recipient;
|
final r = widget.recipient;
|
||||||
_nameCtrl = TextEditingController(text: r?.name ?? "");
|
_nameCtrl = TextEditingController(text: r?.name ?? '');
|
||||||
_emailCtrl = TextEditingController(text: r?.email ?? "");
|
_emailCtrl = TextEditingController(text: r?.email ?? '');
|
||||||
_type = r?.type ?? RecipientType.internal;
|
_type = r?.type ?? RecipientType.internal;
|
||||||
_status = r?.status ?? RecipientStatus.ready;
|
_status = r?.status ?? RecipientStatus.ready;
|
||||||
|
_loadMethods();
|
||||||
if (r?.card != null) _methods[PaymentType.card] = r!.card;
|
|
||||||
if (r?.iban != null) _methods[PaymentType.iban] = r!.iban;
|
|
||||||
if (r?.wallet != null) _methods[PaymentType.wallet] = r!.wallet;
|
|
||||||
if (r?.bank != null) _methods[PaymentType.bankAccount] = r!.bank;
|
|
||||||
if (r?.cryptoAddress != null) _methods[PaymentType.cryptoAddress] = r!.cryptoAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO Change when registration is ready
|
Future<Recipient?> _doSave() async {
|
||||||
void _save() {
|
final recipients = context.read<RecipientsProvider>();
|
||||||
|
final methods = PaymentMethodsProvider();
|
||||||
|
final recipient = widget.recipient == null
|
||||||
|
? await recipients.create(
|
||||||
|
name: _nameCtrl.text,
|
||||||
|
email: _emailCtrl.text,
|
||||||
|
)
|
||||||
|
: widget.recipient!;
|
||||||
|
recipients.setCurrentObject(recipient.id);
|
||||||
|
//TODO : redesign work with recipients / payment methods
|
||||||
|
await methods.loadMethods(context.read<OrganizationsProvider>(), recipient.id);
|
||||||
|
for (final type in _methods.keys) {
|
||||||
|
final data = _methods[type]!;
|
||||||
|
final exising = methods.methods.firstWhereOrNull((m) => m.type == type);
|
||||||
|
if (exising != null) {
|
||||||
|
await methods.updateMethod(exising.copyWith(data: data));
|
||||||
|
} else {
|
||||||
|
await methods.create(
|
||||||
|
reacipientRef: recipient.id,
|
||||||
|
name: getPaymentTypeLabel(context, type),
|
||||||
|
data: data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Change when registration is ready
|
||||||
|
Future<void> _save() async {
|
||||||
if (!_formKey.currentState!.validate() || _methods.isEmpty) {
|
if (!_formKey.currentState!.validate() || _methods.isEmpty) {
|
||||||
AmplitudeService.recipientAddCompleted(
|
notifyUser(context, AppLocalizations.of(context)!.errorSaveRecipient);
|
||||||
_type,
|
|
||||||
_status,
|
|
||||||
_methods.keys.toSet(),
|
|
||||||
);
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(AppLocalizations.of(context)!.recipientFormRule),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final recipient = Recipient(
|
// AmplitudeService.recipientAddCompleted(
|
||||||
name: _nameCtrl.text,
|
// _type,
|
||||||
email: _emailCtrl.text,
|
// _status,
|
||||||
type: _type,
|
// _methods.keys.toSet(),
|
||||||
status: _status,
|
// );
|
||||||
avatarUrl: null,
|
final recipient = await executeActionWithNotification(
|
||||||
card: _methods[PaymentType.card] as CardPaymentMethod?,
|
context: context,
|
||||||
iban: _methods[PaymentType.iban] as IbanPaymentMethod?,
|
action: _doSave,
|
||||||
wallet: _methods[PaymentType.wallet] as WalletPaymentMethod?,
|
errorMessage: AppLocalizations.of(context)!.errorSaveRecipient,
|
||||||
bank: _methods[PaymentType.bankAccount] as RussianBankAccountPaymentMethod?,
|
successMessage: AppLocalizations.of(context)!.recipientFormRule,
|
||||||
cryptoAddress: _methods[PaymentType.cryptoAddress] as CryptoAddressPaymentMethod?,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
widget.onSaved?.call(recipient);
|
widget.onSaved?.call(recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
void dispose() {
|
||||||
return FormView(
|
_methodsProvider.removeListener(_onProviderChanged);
|
||||||
formKey: _formKey,
|
_methodsProvider.dispose();
|
||||||
nameCtrl: _nameCtrl,
|
super.dispose();
|
||||||
emailCtrl: _emailCtrl,
|
|
||||||
type: _type,
|
|
||||||
status: _status,
|
|
||||||
methods: _methods,
|
|
||||||
onTypeChanged: (t) => setState(() => _type = t),
|
|
||||||
onStatusChanged: (s) => setState(() => _status = s),
|
|
||||||
onMethodsChanged: (type, data) {
|
|
||||||
setState(() {
|
|
||||||
if (data != null) {
|
|
||||||
_methods[type] = data;
|
|
||||||
} else {
|
|
||||||
_methods.remove(type);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSave: _save,
|
|
||||||
isEditing: widget.recipient != null,
|
|
||||||
onBack: () {
|
|
||||||
widget.onSaved?.call(null);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onProviderChanged() => setState(() {});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => FormView(
|
||||||
|
formKey: _formKey,
|
||||||
|
nameCtrl: _nameCtrl,
|
||||||
|
emailCtrl: _emailCtrl,
|
||||||
|
type: _type,
|
||||||
|
status: _status,
|
||||||
|
methods: _methods,
|
||||||
|
onTypeChanged: (t) => setState(() => _type = t),
|
||||||
|
onStatusChanged: (s) => setState(() => _status = s),
|
||||||
|
onMethodsChanged: (type, data) {
|
||||||
|
setState(() {
|
||||||
|
if (data != null) {
|
||||||
|
_methods[type] = data;
|
||||||
|
} else {
|
||||||
|
_methods.remove(type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSave: _save,
|
||||||
|
isEditing: widget.recipient != null,
|
||||||
|
onBack: () {
|
||||||
|
widget.onSaved?.call(null);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:pshared/models/payment/type.dart';
|
||||||
import 'package:pshared/models/recipient/status.dart';
|
import 'package:pshared/models/recipient/status.dart';
|
||||||
@@ -20,10 +21,10 @@ class FormView extends StatelessWidget {
|
|||||||
final TextEditingController emailCtrl;
|
final TextEditingController emailCtrl;
|
||||||
final RecipientType type;
|
final RecipientType type;
|
||||||
final RecipientStatus status;
|
final RecipientStatus status;
|
||||||
final Map<PaymentType, Object?> methods;
|
final MethodMap methods;
|
||||||
final ValueChanged<RecipientType> onTypeChanged;
|
final ValueChanged<RecipientType> onTypeChanged;
|
||||||
final ValueChanged<RecipientStatus> onStatusChanged;
|
final ValueChanged<RecipientStatus> onStatusChanged;
|
||||||
final void Function(PaymentType, Object?) onMethodsChanged;
|
final void Function(PaymentType, PaymentMethodData?) onMethodsChanged;
|
||||||
final VoidCallback onSave;
|
final VoidCallback onSave;
|
||||||
final bool isEditing;
|
final bool isEditing;
|
||||||
final VoidCallback onBack;
|
final VoidCallback onBack;
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ class SaveButton extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text ?? AppLocalizations.of(context)!.saveRecipient,
|
text ?? AppLocalizations.of(context)!.saveRecipient,
|
||||||
style: textStyle ??
|
style: textStyle ??
|
||||||
theme.textTheme.labelLarge?.copyWith(
|
theme.textTheme.labelLarge?.copyWith(
|
||||||
color: theme.colorScheme.onPrimary,
|
color: theme.colorScheme.onPrimary,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/permissions/action.dart' as perm;
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/models/resources.dart';
|
||||||
|
import 'package:pshared/provider/permissions.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/address_book/page/recipient/item.dart';
|
import 'package:pweb/pages/address_book/page/recipient/item.dart';
|
||||||
|
|
||||||
@@ -19,9 +24,16 @@ class RecipientAddressBookList extends StatelessWidget {
|
|||||||
this.onDelete,
|
this.onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool _checkPermissions(BuildContext context, Recipient recipient, perm.Action action) {
|
||||||
|
return context.read<PermissionsProvider>().canAccessResource(
|
||||||
|
ResourceType.recipients,
|
||||||
|
action: action,
|
||||||
|
objectRef: recipient.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => ListView.builder(
|
||||||
return ListView.builder(
|
|
||||||
itemCount: filteredRecipients.length,
|
itemCount: filteredRecipients.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final recipient = filteredRecipients[index];
|
final recipient = filteredRecipients[index];
|
||||||
@@ -30,11 +42,14 @@ Widget build(BuildContext context) {
|
|||||||
child: RecipientAddressBookItem(
|
child: RecipientAddressBookItem(
|
||||||
recipient: recipient,
|
recipient: recipient,
|
||||||
onTap: () => onSelected?.call(recipient),
|
onTap: () => onSelected?.call(recipient),
|
||||||
onEdit: () => onEdit?.call(recipient),
|
onEdit: _checkPermissions(context, recipient, perm.Action.update)
|
||||||
onDelete: () => onDelete?.call(recipient),
|
? () => onEdit?.call(recipient)
|
||||||
|
: null,
|
||||||
|
onDelete: _checkPermissions(context, recipient, perm.Action.delete)
|
||||||
|
? () => onDelete?.call(recipient)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
import 'package:pshared/models/recipient/filter.dart';
|
import 'package:pshared/models/recipient/filter.dart';
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/address_book/page/filter_button.dart';
|
import 'package:pweb/pages/address_book/page/filter_button.dart';
|
||||||
import 'package:pweb/pages/address_book/page/header.dart';
|
import 'package:pweb/pages/address_book/page/header.dart';
|
||||||
import 'package:pweb/pages/address_book/page/list.dart';
|
import 'package:pweb/pages/address_book/page/list.dart';
|
||||||
import 'package:pweb/pages/address_book/page/search.dart';
|
import 'package:pweb/pages/address_book/page/search.dart';
|
||||||
import 'package:pweb/providers/recipient.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -18,12 +18,14 @@ class RecipientAddressBookPage extends StatefulWidget {
|
|||||||
final ValueChanged<Recipient> onRecipientSelected;
|
final ValueChanged<Recipient> onRecipientSelected;
|
||||||
final VoidCallback onAddRecipient;
|
final VoidCallback onAddRecipient;
|
||||||
final ValueChanged<Recipient>? onEditRecipient;
|
final ValueChanged<Recipient>? onEditRecipient;
|
||||||
|
final ValueChanged<Recipient>? onDeleteRecipient;
|
||||||
|
|
||||||
const RecipientAddressBookPage({
|
const RecipientAddressBookPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onRecipientSelected,
|
required this.onRecipientSelected,
|
||||||
required this.onAddRecipient,
|
required this.onAddRecipient,
|
||||||
this.onEditRecipient,
|
this.onEditRecipient,
|
||||||
|
this.onDeleteRecipient,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const double _expandedHeight = 550;
|
static const double _expandedHeight = 550;
|
||||||
@@ -42,7 +44,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final provider = context.read<RecipientProvider>();
|
final provider = context.read<RecipientsProvider>();
|
||||||
_searchController = TextEditingController(text: provider.query);
|
_searchController = TextEditingController(text: provider.query);
|
||||||
_searchFocusNode = FocusNode();
|
_searchFocusNode = FocusNode();
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncSearchField(RecipientProvider provider) {
|
void _syncSearchField(RecipientsProvider provider) {
|
||||||
final query = provider.query;
|
final query = provider.query;
|
||||||
if (_searchController.text == query) return;
|
if (_searchController.text == query) return;
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
final loc = AppLocalizations.of(context)!;
|
||||||
final provider = context.watch<RecipientProvider>();
|
final provider = context.watch<RecipientsProvider>();
|
||||||
_syncSearchField(provider);
|
_syncSearchField(provider);
|
||||||
|
|
||||||
if (provider.isLoading) {
|
if (provider.isLoading) {
|
||||||
@@ -125,6 +127,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
|||||||
child: RecipientAddressBookList(
|
child: RecipientAddressBookList(
|
||||||
filteredRecipients: provider.filteredRecipients,
|
filteredRecipients: provider.filteredRecipients,
|
||||||
onEdit: (recipient) => widget.onEditRecipient?.call(recipient),
|
onEdit: (recipient) => widget.onEditRecipient?.call(recipient),
|
||||||
|
onDelete: (recipient) => widget.onDeleteRecipient?.call(recipient),
|
||||||
onSelected: widget.onRecipientSelected,
|
onSelected: widget.onRecipientSelected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,18 +2,22 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
|
|
||||||
class RecipientActions extends StatelessWidget {
|
class RecipientActions extends StatelessWidget {
|
||||||
final VoidCallback onEdit;
|
final VoidCallback? onEdit;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback? onDelete;
|
||||||
|
|
||||||
const RecipientActions({super.key, required this.onEdit, required this.onDelete});
|
const RecipientActions({super.key, required this.onEdit, required this.onDelete});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Row(
|
||||||
return Row(
|
children: [
|
||||||
children: [
|
IconButton(
|
||||||
IconButton(icon: Icon(Icons.edit, color: Theme.of(context).colorScheme.primary), onPressed: onEdit),
|
icon: Icon(Icons.edit, color: Theme.of(context).colorScheme.primary),
|
||||||
IconButton(icon: Icon(Icons.delete, color: Theme.of(context).colorScheme.error), onPressed: onDelete),
|
onPressed: onEdit,
|
||||||
],
|
),
|
||||||
);
|
IconButton(
|
||||||
}
|
icon: Icon(Icons.delete, color: Theme.of(context).colorScheme.error),
|
||||||
|
onPressed: onDelete,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
|
|||||||
class RecipientAddressBookItem extends StatefulWidget {
|
class RecipientAddressBookItem extends StatefulWidget {
|
||||||
final Recipient recipient;
|
final Recipient recipient;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final VoidCallback onEdit;
|
final VoidCallback? onEdit;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback? onDelete;
|
||||||
|
|
||||||
final double borderRadius;
|
final double borderRadius;
|
||||||
final double elevation;
|
final double elevation;
|
||||||
@@ -82,7 +82,9 @@ class _RecipientAddressBookItemState extends State<RecipientAddressBookItem> {
|
|||||||
),
|
),
|
||||||
if (_isHovered)
|
if (_isHovered)
|
||||||
RecipientActions(
|
RecipientActions(
|
||||||
onEdit: widget.onEdit, onDelete: widget.onDelete),
|
onEdit: widget.onEdit,
|
||||||
|
onDelete: widget.onDelete,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: widget.spacingBottom),
|
SizedBox(height: widget.spacingBottom),
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/provider/organizations.dart';
|
||||||
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/address_book/page/recipient/info_row.dart';
|
import 'package:pweb/pages/address_book/page/recipient/info_row.dart';
|
||||||
|
import 'package:pweb/utils/payment/label.dart';
|
||||||
|
|
||||||
|
|
||||||
class RecipientPaymentRow extends StatelessWidget {
|
class RecipientPaymentRow extends StatefulWidget {
|
||||||
final Recipient recipient;
|
final Recipient recipient;
|
||||||
final double spacing;
|
final double spacing;
|
||||||
|
|
||||||
@@ -16,37 +20,43 @@ class RecipientPaymentRow extends StatelessWidget {
|
|||||||
this.spacing = 18
|
this.spacing = 18
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecipientPaymentRow> createState() => _RecipientPaymentRowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipientPaymentRowState extends State<RecipientPaymentRow> {
|
||||||
|
late final PaymentMethodsProvider _methodsProvider;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_methodsProvider = PaymentMethodsProvider()
|
||||||
|
..addListener(_onProviderChanged)
|
||||||
|
..loadMethods(
|
||||||
|
context.read<OrganizationsProvider>(),
|
||||||
|
widget.recipient.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_methodsProvider.removeListener(_onProviderChanged);
|
||||||
|
_methodsProvider.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onProviderChanged() => setState(() {});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator());
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
spacing: spacing,
|
spacing: widget.spacing,
|
||||||
children: [
|
children: _methodsProvider.methods.map((m) => RecipientAddressBookInfoRow(
|
||||||
if (recipient.bank?.accountNumber.isNotEmpty ?? false)
|
type: m.type,
|
||||||
RecipientAddressBookInfoRow(
|
value: getPaymentTypeDescription(context, m),
|
||||||
type: PaymentType.bankAccount,
|
)).toList(),
|
||||||
value: recipient.bank!.accountNumber
|
|
||||||
),
|
|
||||||
if (recipient.card?.pan.isNotEmpty ?? false)
|
|
||||||
RecipientAddressBookInfoRow(
|
|
||||||
type: PaymentType.card,
|
|
||||||
value: recipient.card!.pan
|
|
||||||
),
|
|
||||||
if (recipient.iban?.iban.isNotEmpty ?? false)
|
|
||||||
RecipientAddressBookInfoRow(
|
|
||||||
type: PaymentType.iban,
|
|
||||||
value: recipient.iban!.iban
|
|
||||||
),
|
|
||||||
if (recipient.wallet?.walletId.isNotEmpty ?? false)
|
|
||||||
RecipientAddressBookInfoRow(
|
|
||||||
type: PaymentType.wallet,
|
|
||||||
value: recipient.wallet!.walletId
|
|
||||||
),
|
|
||||||
if (recipient.cryptoAddress?.address.isNotEmpty ?? false)
|
|
||||||
RecipientAddressBookInfoRow(
|
|
||||||
type: PaymentType.cryptoAddress,
|
|
||||||
value: recipient.cryptoAddress!.address,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/models/wallet.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart';
|
||||||
import 'package:pweb/providers/wallets.dart';
|
import 'package:pweb/providers/wallets.dart';
|
||||||
|
|
||||||
@@ -9,7 +10,9 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
|
|
||||||
|
|
||||||
class BalanceWidget extends StatelessWidget {
|
class BalanceWidget extends StatelessWidget {
|
||||||
const BalanceWidget({super.key});
|
final ValueChanged<Wallet> onTopUp;
|
||||||
|
|
||||||
|
const BalanceWidget({super.key, required this.onTopUp});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -30,6 +33,7 @@ class BalanceWidget extends StatelessWidget {
|
|||||||
WalletCarousel(
|
WalletCarousel(
|
||||||
wallets: wallets,
|
wallets: wallets,
|
||||||
onWalletChanged: walletsProvider.selectWallet,
|
onWalletChanged: walletsProvider.selectWallet,
|
||||||
|
onTopUp: onTopUp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import 'package:pweb/providers/wallets.dart';
|
|||||||
|
|
||||||
class WalletCard extends StatelessWidget {
|
class WalletCard extends StatelessWidget {
|
||||||
final Wallet wallet;
|
final Wallet wallet;
|
||||||
|
final VoidCallback onTopUp;
|
||||||
|
|
||||||
const WalletCard({
|
const WalletCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.wallet,
|
required this.wallet,
|
||||||
|
required this.onTopUp,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -43,7 +45,7 @@ class WalletCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
BalanceAddFunds(
|
BalanceAddFunds(
|
||||||
onTopUp: () {
|
onTopUp: () {
|
||||||
// TODO: Implement top-up functionality
|
onTopUp();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -51,4 +53,4 @@ class WalletCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import 'package:pweb/providers/carousel.dart';
|
|||||||
class WalletCarousel extends StatefulWidget {
|
class WalletCarousel extends StatefulWidget {
|
||||||
final List<Wallet> wallets;
|
final List<Wallet> wallets;
|
||||||
final ValueChanged<Wallet> onWalletChanged;
|
final ValueChanged<Wallet> onWalletChanged;
|
||||||
|
final ValueChanged<Wallet> onTopUp;
|
||||||
|
|
||||||
const WalletCarousel({
|
const WalletCarousel({
|
||||||
super.key,
|
super.key,
|
||||||
required this.wallets,
|
required this.wallets,
|
||||||
required this.onWalletChanged,
|
required this.onWalletChanged,
|
||||||
|
required this.onTopUp,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -33,6 +35,11 @@ class _WalletCarouselState extends State<WalletCarousel> {
|
|||||||
_pageController = PageController(
|
_pageController = PageController(
|
||||||
viewportFraction: WalletCardConfig.viewportFraction,
|
viewportFraction: WalletCardConfig.viewportFraction,
|
||||||
);
|
);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (widget.wallets.isNotEmpty) {
|
||||||
|
widget.onWalletChanged(widget.wallets[_currentPage]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -83,7 +90,10 @@ class _WalletCarouselState extends State<WalletCarousel> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: WalletCardConfig.cardPadding,
|
padding: WalletCardConfig.cardPadding,
|
||||||
child: WalletCard(wallet: widget.wallets[index]),
|
child: WalletCard(
|
||||||
|
wallet: widget.wallets[index],
|
||||||
|
onTopUp: () => widget.onTopUp(widget.wallets[index]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -110,4 +120,4 @@ class _WalletCarouselState extends State<WalletCarousel> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.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:pweb/models/wallet.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/balance.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/balance.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/buttons.dart';
|
import 'package:pweb/pages/dashboard/buttons/buttons.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/title.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/title.dart';
|
||||||
@@ -22,11 +23,13 @@ class AppSpacing {
|
|||||||
class DashboardPage extends StatefulWidget {
|
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;
|
||||||
|
|
||||||
const DashboardPage({
|
const DashboardPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onRecipientSelected,
|
required this.onRecipientSelected,
|
||||||
required this.onGoToPaymentWithoutRecipient,
|
required this.onGoToPaymentWithoutRecipient,
|
||||||
|
required this.onTopUp,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -75,7 +78,9 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.medium),
|
const SizedBox(height: AppSpacing.medium),
|
||||||
BalanceWidget(),
|
BalanceWidget(
|
||||||
|
onTopUp: widget.onTopUp,
|
||||||
|
),
|
||||||
const SizedBox(height: AppSpacing.small),
|
const SizedBox(height: AppSpacing.small),
|
||||||
if (_showContainerMultiple) TitleMultiplePayout(),
|
if (_showContainerMultiple) TitleMultiplePayout(),
|
||||||
const SizedBox(height: AppSpacing.medium),
|
const SizedBox(height: AppSpacing.medium),
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ class PaymentInfoRow extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Row(
|
||||||
return Row(
|
children: [
|
||||||
children: [
|
Text(label, style: Theme.of(context).textTheme.bodySmall),
|
||||||
Text(label, style: Theme.of(context).textTheme.bodySmall),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
Text(value, style: Theme.of(context).textTheme.bodySmall),
|
||||||
Text(value, style: Theme.of(context).textTheme.bodySmall),
|
],
|
||||||
],
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,70 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/provider/organizations.dart';
|
||||||
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
|
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart';
|
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart';
|
||||||
import 'package:pweb/utils/payment/label.dart';
|
import 'package:pweb/utils/payment/label.dart';
|
||||||
|
|
||||||
|
|
||||||
class RecipientItem extends StatelessWidget {
|
class RecipientItem extends StatefulWidget {
|
||||||
final Recipient recipient;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
static const double _horizontalPadding = 16.0;
|
static const double _horizontalPadding = 16.0;
|
||||||
static const double _verticalPadding = 8.0;
|
static const double _verticalPadding = 8.0;
|
||||||
static const double _avatarRadius = 20;
|
static const double _avatarRadius = 20;
|
||||||
static const double _spacingWidth = 12;
|
static const double _spacingWidth = 12;
|
||||||
|
|
||||||
|
final Recipient recipient;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const RecipientItem({
|
const RecipientItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.recipient,
|
required this.recipient,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecipientItem> createState() => _RecipientItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipientItemState extends State<RecipientItem> {
|
||||||
|
late PaymentMethodsProvider _methodsProvider;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_methodsProvider = PaymentMethodsProvider()
|
||||||
|
..addListener(_onProviderChanged)
|
||||||
|
..loadMethods(
|
||||||
|
context.read<OrganizationsProvider>(),
|
||||||
|
widget.recipient.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_methodsProvider.removeListener(_onProviderChanged);
|
||||||
|
_methodsProvider.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onProviderChanged() => setState(() {});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator());
|
||||||
|
|
||||||
|
final recipient = widget.recipient;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: widget.onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: _horizontalPadding,
|
horizontal: RecipientItem._horizontalPadding,
|
||||||
vertical: _verticalPadding,
|
vertical: RecipientItem._verticalPadding,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -42,43 +76,20 @@ class RecipientItem extends StatelessWidget {
|
|||||||
isVisible: false,
|
isVisible: false,
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
avatarUrl: recipient.avatarUrl,
|
avatarUrl: recipient.avatarUrl,
|
||||||
avatarRadius: _avatarRadius,
|
avatarRadius: RecipientItem._avatarRadius,
|
||||||
nameStyle: Theme.of(context).textTheme.bodyMedium,
|
nameStyle: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
title: Text(recipient.name),
|
title: Text(recipient.name),
|
||||||
subtitle: Text(recipient.email),
|
subtitle: Text(recipient.email),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: _spacingWidth),
|
const SizedBox(width: RecipientItem._spacingWidth),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: _methodsProvider.methods.map((m) => PaymentInfoRow(
|
||||||
if (recipient.bank?.accountNumber.isNotEmpty == true)
|
label: getPaymentTypeLabel(context, m.type),
|
||||||
PaymentInfoRow(
|
value: getPaymentTypeDescription(context, m),
|
||||||
label: getPaymentTypeLabel(context, PaymentType.bankAccount),
|
)).toList(),
|
||||||
value: recipient.bank!.accountNumber,
|
|
||||||
),
|
|
||||||
if (recipient.card?.pan.isNotEmpty == true)
|
|
||||||
PaymentInfoRow(
|
|
||||||
label: getPaymentTypeLabel(context, PaymentType.card),
|
|
||||||
value: recipient.card!.pan,
|
|
||||||
),
|
|
||||||
if (recipient.iban?.iban.isNotEmpty == true)
|
|
||||||
PaymentInfoRow(
|
|
||||||
label: getPaymentTypeLabel(context, PaymentType.iban),
|
|
||||||
value: recipient.iban!.iban,
|
|
||||||
),
|
|
||||||
if (recipient.wallet?.walletId.isNotEmpty == true)
|
|
||||||
PaymentInfoRow(
|
|
||||||
label: getPaymentTypeLabel(context, PaymentType.wallet),
|
|
||||||
value: recipient.wallet!.walletId,
|
|
||||||
),
|
|
||||||
if (recipient.cryptoAddress?.address.isNotEmpty == true)
|
|
||||||
PaymentInfoRow(
|
|
||||||
label: getPaymentTypeLabel(context, PaymentType.cryptoAddress),
|
|
||||||
value: recipient.cryptoAddress!.address,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/address_book/page/search.dart';
|
import 'package:pweb/pages/address_book/page/search.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart';
|
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/widget.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/single/adress_book/short_list.dart';
|
import 'package:pweb/pages/dashboard/payouts/single/adress_book/short_list.dart';
|
||||||
import 'package:pweb/providers/recipient.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final provider = context.read<RecipientProvider>();
|
final provider = context.read<RecipientsProvider>();
|
||||||
_searchController = TextEditingController(text: provider.query);
|
_searchController = TextEditingController(text: provider.query);
|
||||||
|
|
||||||
_searchController.addListener(() {
|
_searchController.addListener(() {
|
||||||
@@ -57,7 +57,7 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final loc = AppLocalizations.of(context)!;
|
final loc = AppLocalizations.of(context)!;
|
||||||
final provider = context.watch<RecipientProvider>();
|
final provider = context.watch<RecipientsProvider>();
|
||||||
|
|
||||||
if (provider.isLoading) {
|
if (provider.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@@ -88,14 +88,14 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
|
|||||||
const SizedBox(height: _spacingBetween),
|
const SizedBox(height: _spacingBetween),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _isExpanded
|
child: _isExpanded
|
||||||
? LongListAdressBookPayout(
|
? LongListAdressBookPayout(
|
||||||
filteredRecipients: provider.filteredRecipients,
|
filteredRecipients: provider.filteredRecipients,
|
||||||
onSelected: widget.onSelected,
|
onSelected: widget.onSelected,
|
||||||
)
|
)
|
||||||
: ShortListAdressBookPayout(
|
: ShortListAdressBookPayout(
|
||||||
recipients: provider.recipients,
|
recipients: provider.recipients,
|
||||||
onSelected: widget.onSelected,
|
onSelected: widget.onSelected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:pshared/models/payment/type.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/payment_methods/form.dart';
|
import 'package:pweb/pages/payment_methods/form.dart';
|
||||||
@@ -12,7 +13,7 @@ class PaymentDetailsSection extends StatelessWidget {
|
|||||||
final bool isEditable;
|
final bool isEditable;
|
||||||
final VoidCallback? onToggle;
|
final VoidCallback? onToggle;
|
||||||
final PaymentType? selectedType;
|
final PaymentType? selectedType;
|
||||||
final Object? data;
|
final PaymentMethodData? data;
|
||||||
|
|
||||||
const PaymentDetailsSection({
|
const PaymentDetailsSection({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -50,9 +51,7 @@ class PaymentDetailsSection extends StatelessWidget {
|
|||||||
const SizedBox(height: toggleSpacing),
|
const SizedBox(height: toggleSpacing),
|
||||||
AnimatedCrossFade(
|
AnimatedCrossFade(
|
||||||
duration: animationDuration,
|
duration: animationDuration,
|
||||||
crossFadeState: isFormVisible
|
crossFadeState: isFormVisible ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||||
? CrossFadeState.showFirst
|
|
||||||
: CrossFadeState.showSecond,
|
|
||||||
firstChild: PaymentMethodForm(
|
firstChild: PaymentMethodForm(
|
||||||
key: const ValueKey('formVisible'),
|
key: const ValueKey('formVisible'),
|
||||||
isEditable: isEditable,
|
isEditable: isEditable,
|
||||||
|
|||||||
@@ -17,15 +17,34 @@ class AccountLoader extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Consumer<AccountProvider>(builder: (context, provider, _) {
|
Widget build(BuildContext context) => Consumer<AccountProvider>(builder: (context, provider, _) {
|
||||||
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
|
if (provider.account != null) {
|
||||||
if (provider.error != null) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
postNotifyUserOfErrorX(
|
});
|
||||||
context: context,
|
return child;
|
||||||
errorSituation: AppLocalizations.of(context)!.errorLogin,
|
|
||||||
exception: provider.error!,
|
|
||||||
);
|
|
||||||
navigateAndReplace(context, Pages.login);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider.error != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
postNotifyUserOfErrorX(
|
||||||
|
context: context,
|
||||||
|
errorSituation: AppLocalizations.of(context)!.errorLogin,
|
||||||
|
exception: provider.error!,
|
||||||
|
);
|
||||||
|
navigateAndReplace(context, Pages.login);
|
||||||
|
});
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.restoreFuture == null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
provider.restoreIfPossible().catchError((error, stack) {
|
||||||
|
debugPrint('Account restore failed: $error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
|
||||||
if (provider.account == null) {
|
if (provider.account == null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login));
|
WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login));
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:pshared/models/payment/methods/card.dart';
|
import 'package:pshared/models/payment/methods/card.dart';
|
||||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||||
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
import 'package:pshared/models/payment/methods/iban.dart';
|
import 'package:pshared/models/payment/methods/iban.dart';
|
||||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||||
@@ -16,8 +17,8 @@ import 'package:pweb/pages/payment_methods/add/wallet.dart';
|
|||||||
|
|
||||||
class PaymentMethodForm extends StatelessWidget {
|
class PaymentMethodForm extends StatelessWidget {
|
||||||
final PaymentType? selectedType;
|
final PaymentType? selectedType;
|
||||||
final ValueChanged<Object?> onChanged;
|
final ValueChanged<PaymentMethodData?> onChanged;
|
||||||
final Object? initialData;
|
final PaymentMethodData? initialData;
|
||||||
final bool isEditable;
|
final bool isEditable;
|
||||||
|
|
||||||
const PaymentMethodForm({
|
const PaymentMethodForm({
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/methods/type.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/payment/methods/type.dart';
|
||||||
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
|
||||||
import 'package:pweb/providers/payment_methods.dart';
|
|
||||||
import 'package:pweb/utils/payment/dropdown.dart';
|
import 'package:pweb/utils/payment/dropdown.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodSelector extends StatelessWidget {
|
class PaymentMethodSelector extends StatelessWidget {
|
||||||
final PaymentMethodsProvider methodsProvider;
|
|
||||||
final ValueChanged<PaymentMethod> onMethodChanged;
|
final ValueChanged<PaymentMethod> onMethodChanged;
|
||||||
|
|
||||||
const PaymentMethodSelector({
|
const PaymentMethodSelector({
|
||||||
super.key,
|
super.key,
|
||||||
required this.methodsProvider,
|
|
||||||
required this.onMethodChanged,
|
required this.onMethodChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Consumer<PaymentMethodsProvider>(builder:(context, provider, _) => PaymentMethodDropdown(
|
||||||
return PaymentMethodDropdown(
|
methods: provider.methods,
|
||||||
methods: methodsProvider.methods,
|
initialValue: provider.currentObject,
|
||||||
initialValue: methodsProvider.selectedMethod,
|
onChanged: onMethodChanged,
|
||||||
onChanged: onMethodChanged,
|
));
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/providers/payment_flow_provider.dart';
|
import 'package:pweb/providers/payment_flow_provider.dart';
|
||||||
import 'package:pweb/pages/payment_methods/widgets/payment_page_body.dart';
|
import 'package:pweb/pages/payment_methods/widgets/payment_page_body.dart';
|
||||||
import 'package:pweb/providers/page_selector.dart';
|
import 'package:pweb/providers/page_selector.dart';
|
||||||
import 'package:pweb/providers/payment_methods.dart';
|
|
||||||
import 'package:pweb/providers/recipient.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentPage extends StatefulWidget {
|
class PaymentPage extends StatefulWidget {
|
||||||
@@ -48,42 +47,32 @@ class _PaymentPageState extends State<PaymentPage> {
|
|||||||
|
|
||||||
void _initializePaymentPage() {
|
void _initializePaymentPage() {
|
||||||
final pageSelector = context.read<PageSelectorProvider>();
|
final pageSelector = context.read<PageSelectorProvider>();
|
||||||
final methodsProvider = context.read<PaymentMethodsProvider>();
|
|
||||||
final recipientProvider = context.read<RecipientProvider>();
|
|
||||||
|
|
||||||
pageSelector.handleWalletAutoSelection();
|
pageSelector.handleWalletAutoSelection();
|
||||||
|
|
||||||
if (methodsProvider.methods.isEmpty && !methodsProvider.isLoading) {
|
|
||||||
methodsProvider.loadMethods();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recipientProvider.recipients.isEmpty && !recipientProvider.isLoading) {
|
|
||||||
recipientProvider.loadRecipients();
|
|
||||||
}
|
|
||||||
|
|
||||||
_flowProvider.syncWithSelector(pageSelector);
|
_flowProvider.syncWithSelector(pageSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSearchChanged(String query) {
|
void _handleSearchChanged(String query) {
|
||||||
context.read<RecipientProvider>().setQuery(query);
|
context.read<RecipientsProvider>().setQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleRecipientSelected(Recipient recipient) {
|
void _handleRecipientSelected(Recipient recipient) {
|
||||||
final pageSelector = context.read<PageSelectorProvider>();
|
final pageSelector = context.read<PageSelectorProvider>();
|
||||||
final recipientProvider = context.read<RecipientProvider>();
|
final recipientProvider = context.read<RecipientsProvider>();
|
||||||
|
|
||||||
recipientProvider.selectRecipient(recipient);
|
recipientProvider.setCurrentObject(recipient.id);
|
||||||
pageSelector.selectRecipient(recipient);
|
pageSelector.selectRecipient(context, recipient);
|
||||||
_flowProvider.reset(pageSelector);
|
_flowProvider.reset(pageSelector);
|
||||||
_clearSearchField();
|
_clearSearchField();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleRecipientCleared() {
|
void _handleRecipientCleared() {
|
||||||
final pageSelector = context.read<PageSelectorProvider>();
|
final pageSelector = context.read<PageSelectorProvider>();
|
||||||
final recipientProvider = context.read<RecipientProvider>();
|
final recipientProvider = context.read<RecipientsProvider>();
|
||||||
|
|
||||||
recipientProvider.selectRecipient(null);
|
recipientProvider.setCurrentObject(null);
|
||||||
pageSelector.selectRecipient(null);
|
pageSelector.selectRecipient(context, null);
|
||||||
_flowProvider.reset(pageSelector);
|
_flowProvider.reset(pageSelector);
|
||||||
_clearSearchField();
|
_clearSearchField();
|
||||||
}
|
}
|
||||||
@@ -91,7 +80,7 @@ class _PaymentPageState extends State<PaymentPage> {
|
|||||||
void _clearSearchField() {
|
void _clearSearchField() {
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
_searchFocusNode.unfocus();
|
_searchFocusNode.unfocus();
|
||||||
context.read<RecipientProvider>().setQuery('');
|
context.read<RecipientsProvider>().setQuery('');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSendPayment() {
|
void _handleSendPayment() {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class PaymentMethodTile extends StatelessWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: method.isEnabled ? 1 : 0.5,
|
opacity: method.isArchived ? 1 : 0.5,
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -41,11 +41,12 @@ class PaymentMethodTile extends StatelessWidget {
|
|||||||
onTap: makeMain,
|
onTap: makeMain,
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(method.label)),
|
Expanded(child: Text(method.name)),
|
||||||
Text(
|
if (method.description != null)
|
||||||
method.details,
|
Text(
|
||||||
style: theme.textTheme.bodySmall,
|
method.description!,
|
||||||
),
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
@@ -73,12 +74,10 @@ class PaymentMethodTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEnabledSwitch() {
|
Widget _buildEnabledSwitch() => Switch.adaptive(
|
||||||
return Switch.adaptive(
|
value: method.isArchived,
|
||||||
value: method.isEnabled,
|
onChanged: toggleEnabled,
|
||||||
onChanged: toggleEnabled,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPopupMenu(AppLocalizations l10n) {
|
Widget _buildPopupMenu(AppLocalizations l10n) {
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/payment/methods/data.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';
|
||||||
|
|
||||||
@@ -31,9 +32,9 @@ class PaymentInfoSection extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final loc = AppLocalizations.of(context)!;
|
final loc = AppLocalizations.of(context)!;
|
||||||
final hasRecipient = recipient != null;
|
final hasRecipient = recipient != null;
|
||||||
final availableTypes = hasRecipient
|
final MethodMap availableTypes = hasRecipient
|
||||||
? pageSelector.getAvailablePaymentTypes()
|
? pageSelector.getAvailablePaymentTypes()
|
||||||
: {for (final type in PaymentType.values) type: type};
|
: {for (final type in PaymentType.values) type: null};
|
||||||
|
|
||||||
if (hasRecipient && availableTypes.isEmpty) {
|
if (hasRecipient && availableTypes.isEmpty) {
|
||||||
return Text(loc.recipientNoPaymentDetails);
|
return Text(loc.recipientNoPaymentDetails);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/payment_methods/header.dart';
|
import 'package:pweb/pages/payment_methods/header.dart';
|
||||||
import 'package:pweb/pages/payment_methods/method_selector.dart';
|
import 'package:pweb/pages/payment_methods/method_selector.dart';
|
||||||
@@ -12,9 +15,8 @@ import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart';
|
|||||||
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
|
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
|
||||||
import 'package:pweb/providers/page_selector.dart';
|
import 'package:pweb/providers/page_selector.dart';
|
||||||
import 'package:pweb/providers/payment_flow_provider.dart';
|
import 'package:pweb/providers/payment_flow_provider.dart';
|
||||||
import 'package:pweb/providers/payment_methods.dart';
|
|
||||||
import 'package:pweb/providers/recipient.dart';
|
|
||||||
import 'package:pweb/utils/dimensions.dart';
|
import 'package:pweb/utils/dimensions.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ class PaymentPageBody extends StatelessWidget {
|
|||||||
final dimensions = AppDimensions();
|
final dimensions = AppDimensions();
|
||||||
final pageSelector = context.watch<PageSelectorProvider>();
|
final pageSelector = context.watch<PageSelectorProvider>();
|
||||||
final methodsProvider = context.watch<PaymentMethodsProvider>();
|
final methodsProvider = context.watch<PaymentMethodsProvider>();
|
||||||
final recipientProvider = context.watch<RecipientProvider>();
|
final recipientProvider = context.watch<RecipientsProvider>();
|
||||||
final flowProvider = context.watch<PaymentFlowProvider>();
|
final flowProvider = context.watch<PaymentFlowProvider>();
|
||||||
final recipient = pageSelector.selectedRecipient;
|
final recipient = pageSelector.selectedRecipient;
|
||||||
final loc = AppLocalizations.of(context)!;
|
final loc = AppLocalizations.of(context)!;
|
||||||
@@ -79,8 +81,7 @@ class PaymentPageBody extends StatelessWidget {
|
|||||||
SectionTitle(loc.sourceOfFunds),
|
SectionTitle(loc.sourceOfFunds),
|
||||||
SizedBox(height: dimensions.paddingSmall),
|
SizedBox(height: dimensions.paddingSmall),
|
||||||
PaymentMethodSelector(
|
PaymentMethodSelector(
|
||||||
methodsProvider: methodsProvider,
|
onMethodChanged: (m) => methodsProvider.setCurrentObject(m.id),
|
||||||
onMethodChanged: methodsProvider.selectMethod,
|
|
||||||
),
|
),
|
||||||
SizedBox(height: dimensions.paddingXLarge),
|
SizedBox(height: dimensions.paddingXLarge),
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ class PaymentBackButton extends StatelessWidget {
|
|||||||
if (onBack != null) {
|
if (onBack != null) {
|
||||||
onBack!(pageSelector.selectedRecipient);
|
onBack!(pageSelector.selectedRecipient);
|
||||||
} else {
|
} else {
|
||||||
pageSelector.goBackFromPayment();
|
pageSelector.goBackFromPayment(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/address_book/page/search.dart';
|
import 'package:pweb/pages/address_book/page/search.dart';
|
||||||
import 'package:pweb/pages/payment_methods/widgets/card.dart';
|
import 'package:pweb/pages/payment_methods/widgets/card.dart';
|
||||||
import 'package:pweb/pages/payment_methods/widgets/search.dart';
|
import 'package:pweb/pages/payment_methods/widgets/search.dart';
|
||||||
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
|
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
|
||||||
import 'package:pweb/providers/recipient.dart';
|
|
||||||
import 'package:pweb/utils/dimensions.dart';
|
import 'package:pweb/utils/dimensions.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
@@ -15,7 +15,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
class RecipientSection extends StatelessWidget {
|
class RecipientSection extends StatelessWidget {
|
||||||
final Recipient? recipient;
|
final Recipient? recipient;
|
||||||
final AppDimensions dimensions;
|
final AppDimensions dimensions;
|
||||||
final RecipientProvider recipientProvider;
|
final RecipientsProvider recipientProvider;
|
||||||
final TextEditingController searchController;
|
final TextEditingController searchController;
|
||||||
final FocusNode searchFocusNode;
|
final FocusNode searchFocusNode;
|
||||||
final ValueChanged<String> onSearchChanged;
|
final ValueChanged<String> onSearchChanged;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/providers/recipient.dart';
|
|
||||||
import 'package:pweb/utils/dimensions.dart';
|
import 'package:pweb/utils/dimensions.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
@@ -10,7 +10,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
|
|
||||||
class RecipientSearchResults extends StatelessWidget {
|
class RecipientSearchResults extends StatelessWidget {
|
||||||
final AppDimensions dimensions;
|
final AppDimensions dimensions;
|
||||||
final RecipientProvider recipientProvider;
|
final RecipientsProvider recipientProvider;
|
||||||
final ValueChanged<Recipient> onRecipientSelected;
|
final ValueChanged<Recipient> onRecipientSelected;
|
||||||
|
|
||||||
const RecipientSearchResults({
|
const RecipientSearchResults({
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/models/payment/methods/data.dart';
|
import 'package:pshared/models/payment/methods/data.dart';
|
||||||
import 'package:pshared/models/payment/methods/type.dart';
|
import 'package:pshared/models/payment/methods/type.dart';
|
||||||
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
|
||||||
import 'package:pweb/providers/payment_methods.dart';
|
|
||||||
import 'package:pweb/pages/payment_methods/add/widget.dart';
|
import 'package:pweb/pages/payment_methods/add/widget.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
@@ -16,18 +16,10 @@ class PaymentConfigController {
|
|||||||
|
|
||||||
PaymentConfigController(this.context);
|
PaymentConfigController(this.context);
|
||||||
|
|
||||||
void loadMethods() {
|
Future<void> addMethod() async => showDialog<PaymentMethodData>(
|
||||||
context.read<PaymentMethodsProvider>().loadMethods();
|
context: context,
|
||||||
}
|
builder: (_) => const AddPaymentMethodDialog(),
|
||||||
|
);
|
||||||
Future<void> addMethod() async {
|
|
||||||
final methodsProvider = context.read<PaymentMethodsProvider>();
|
|
||||||
await showDialog<PaymentMethodData>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => const AddPaymentMethodDialog(),
|
|
||||||
);
|
|
||||||
methodsProvider.loadMethods();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> editMethod(PaymentMethod method) async {
|
Future<void> editMethod(PaymentMethod method) async {
|
||||||
// TODO: implement edit functionality
|
// TODO: implement edit functionality
|
||||||
@@ -55,12 +47,12 @@ class PaymentConfigController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
methodsProvider.deleteMethod(method);
|
methodsProvider.delete(method.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleEnabled(PaymentMethod method, bool value) {
|
void toggleEnabled(PaymentMethod method, bool value) {
|
||||||
context.read<PaymentMethodsProvider>().toggleEnabled(method, value);
|
context.read<PaymentMethodsProvider>().setArchivedMethod(method: method, newIsArchived: value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void makeMain(PaymentMethod method) {
|
void makeMain(PaymentMethod method) {
|
||||||
@@ -68,6 +60,7 @@ class PaymentConfigController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void reorder(int oldIndex, int newIndex) {
|
void reorder(int oldIndex, int newIndex) {
|
||||||
context.read<PaymentMethodsProvider>().reorderMethods(oldIndex, newIndex);
|
// TODO: rimplement on top of Indexable
|
||||||
|
// context.read<PaymentMethodsProvider>().reorderMethods(oldIndex, newIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/payment_methods/title.dart';
|
import 'package:pweb/pages/payment_methods/title.dart';
|
||||||
import 'package:pweb/pages/payout_page/methods/controller.dart';
|
import 'package:pweb/pages/payout_page/methods/controller.dart';
|
||||||
import 'package:pweb/providers/payment_methods.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentConfigList extends StatelessWidget {
|
class PaymentConfigList extends StatelessWidget {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class _MethodsWidgetState extends State<MethodsWidget> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
controller = PaymentConfigController(context);
|
controller = PaymentConfigController(context);
|
||||||
controller.loadMethods();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/models/wallet.dart';
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/models/wallet.dart';
|
||||||
import 'package:pweb/pages/payout_page/methods/widget.dart';
|
import 'package:pweb/pages/payout_page/methods/widget.dart';
|
||||||
import 'package:pweb/pages/payout_page/wallet/wigets.dart';
|
import 'package:pweb/pages/payout_page/wallet/wigets.dart';
|
||||||
import 'package:pweb/providers/payment_methods.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class SendPayoutButton extends StatelessWidget {
|
|||||||
final wallet = walletsProvider.selectedWallet;
|
final wallet = walletsProvider.selectedWallet;
|
||||||
|
|
||||||
if (wallet != null) {
|
if (wallet != null) {
|
||||||
pageSelectorProvider.startPaymentFromWallet(wallet);
|
pageSelectorProvider.startPaymentFromWallet(context, wallet);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(loc.payoutNavSendPayout),
|
child: Text(loc.payoutNavSendPayout),
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/providers/page_selector.dart';
|
||||||
|
import 'package:pweb/providers/wallets.dart';
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -15,9 +19,14 @@ class TopUpButton extends StatelessWidget{
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
final wallet = context.read<WalletsProvider>().selectedWallet;
|
||||||
SnackBar(content: Text(loc.addFunctionality)),
|
if (wallet == null) {
|
||||||
);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(loc.noWalletSelected)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.read<PageSelectorProvider>().openWalletTopUp(context, wallet);
|
||||||
},
|
},
|
||||||
child: Text(loc.topUpBalance),
|
child: Text(loc.topUpBalance),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/provider/locale.dart';
|
import 'package:pshared/provider/locale.dart';
|
||||||
|
|
||||||
import 'package:pweb/services/amplitude.dart';
|
// import 'package:pweb/services/amplitude.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class LocalePicker extends StatelessWidget {
|
|||||||
onChanged: (locale) {
|
onChanged: (locale) {
|
||||||
if (locale != null) {
|
if (locale != null) {
|
||||||
localeProvider.setLocale(locale);
|
localeProvider.setLocale(locale);
|
||||||
AmplitudeService.localeChanged(locale);
|
// AmplitudeService.localeChanged(locale);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user