Compare commits
3 Commits
0e48d2a318
...
SEND004
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32cccc7895 | ||
| 81d2db394b | |||
|
|
8d6a302cb8 |
@@ -1,19 +1,17 @@
|
||||
package srequest
|
||||
|
||||
import orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||
|
||||
type QuotePayment struct {
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Intent *orchestratorv1.PaymentIntent `json:"intent"`
|
||||
PreviewOnly bool `json:"previewOnly"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Intent *PaymentIntent `json:"intent"`
|
||||
PreviewOnly bool `json:"previewOnly"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type InitiatePayment struct {
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Intent *orchestratorv1.PaymentIntent `json:"intent"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
FeeQuoteToken string `json:"feeQuoteToken,omitempty"`
|
||||
FxQuoteRef string `json:"fxQuoteRef,omitempty"`
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Intent *PaymentIntent `json:"intent"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
FeeQuoteToken string `json:"feeQuoteToken,omitempty"`
|
||||
FxQuoteRef string `json:"fxQuoteRef,omitempty"`
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
}
|
||||
|
||||
103
api/server/interface/api/srequest/payment_types.go
Normal file
103
api/server/interface/api/srequest/payment_types.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package srequest
|
||||
|
||||
// PaymentKind mirrors the orchestrator payment kinds without importing generated proto types.
|
||||
type PaymentKind int32
|
||||
|
||||
const (
|
||||
PaymentKindUnspecified PaymentKind = 0
|
||||
PaymentKindPayout PaymentKind = 1
|
||||
PaymentKindInternalTransfer PaymentKind = 2
|
||||
PaymentKindFxConversion PaymentKind = 3
|
||||
)
|
||||
|
||||
// FXSide mirrors the common FX side enum.
|
||||
type FXSide int32
|
||||
|
||||
const (
|
||||
FXSideUnspecified FXSide = 0
|
||||
FXSideBuyBaseSellQuote FXSide = 1
|
||||
FXSideSellBaseBuyQuote FXSide = 2
|
||||
)
|
||||
|
||||
// ChainNetwork mirrors the chain network enum used by managed wallets.
|
||||
type ChainNetwork int32
|
||||
|
||||
const (
|
||||
ChainNetworkUnspecified ChainNetwork = 0
|
||||
ChainNetworkEthereumMainnet ChainNetwork = 1
|
||||
ChainNetworkArbitrumOne ChainNetwork = 2
|
||||
ChainNetworkOtherEVM ChainNetwork = 3
|
||||
)
|
||||
|
||||
// InsufficientNetPolicy mirrors the fee engine policy override.
|
||||
type InsufficientNetPolicy int32
|
||||
|
||||
const (
|
||||
InsufficientNetPolicyUnspecified InsufficientNetPolicy = 0
|
||||
InsufficientNetPolicyBlockPosting InsufficientNetPolicy = 1
|
||||
InsufficientNetPolicySweepOrgCash InsufficientNetPolicy = 2
|
||||
InsufficientNetPolicyInvoiceLater InsufficientNetPolicy = 3
|
||||
)
|
||||
|
||||
type PaymentIntent struct {
|
||||
Kind PaymentKind `json:"kind,omitempty"`
|
||||
Source *PaymentEndpoint `json:"source,omitempty"`
|
||||
Destination *PaymentEndpoint `json:"destination,omitempty"`
|
||||
Amount *Money `json:"amount,omitempty"`
|
||||
RequiresFX bool `json:"requires_fx,omitempty"`
|
||||
FX *FXIntent `json:"fx,omitempty"`
|
||||
FeePolicy *PolicyOverrides `json:"fee_policy,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentEndpoint struct {
|
||||
Ledger *LedgerEndpoint `json:"ledger,omitempty"`
|
||||
ManagedWallet *ManagedWalletEndpoint `json:"managed_wallet,omitempty"`
|
||||
ExternalChain *ExternalChainEndpoint `json:"external_chain,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type LedgerEndpoint struct {
|
||||
LedgerAccountRef string `json:"ledger_account_ref"`
|
||||
ContraLedgerAccountRef string `json:"contra_ledger_account_ref,omitempty"`
|
||||
}
|
||||
|
||||
type ManagedWalletEndpoint struct {
|
||||
ManagedWalletRef string `json:"managed_wallet_ref"`
|
||||
Asset *Asset `json:"asset,omitempty"`
|
||||
}
|
||||
|
||||
type ExternalChainEndpoint struct {
|
||||
Asset *Asset `json:"asset,omitempty"`
|
||||
Address string `json:"address"`
|
||||
Memo string `json:"memo,omitempty"`
|
||||
}
|
||||
|
||||
type Asset struct {
|
||||
Chain ChainNetwork `json:"chain"`
|
||||
TokenSymbol string `json:"token_symbol"`
|
||||
ContractAddress string `json:"contract_address,omitempty"`
|
||||
}
|
||||
|
||||
type Money struct {
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type CurrencyPair struct {
|
||||
Base string `json:"base"`
|
||||
Quote string `json:"quote"`
|
||||
}
|
||||
|
||||
type FXIntent struct {
|
||||
Pair *CurrencyPair `json:"pair,omitempty"`
|
||||
Side FXSide `json:"side,omitempty"`
|
||||
Firm bool `json:"firm,omitempty"`
|
||||
TTLms int64 `json:"ttl_ms,omitempty"`
|
||||
PreferredProvider string `json:"preferred_provider,omitempty"`
|
||||
MaxAgeMs int32 `json:"max_age_ms,omitempty"`
|
||||
}
|
||||
|
||||
type PolicyOverrides struct {
|
||||
InsufficientNet InsufficientNetPolicy `json:"insufficient_net,omitempty"`
|
||||
}
|
||||
175
api/server/internal/server/paymentapiimp/mapper.go
Normal file
175
api/server/internal/server/paymentapiimp/mapper.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package paymentapiimp
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
)
|
||||
|
||||
func mapPaymentIntent(intent *srequest.PaymentIntent) (*orchestratorv1.PaymentIntent, error) {
|
||||
if intent == nil {
|
||||
return nil, merrors.InvalidArgument("intent is required")
|
||||
}
|
||||
|
||||
source, err := mapPaymentEndpoint(intent.Source, "source")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
destination, err := mapPaymentEndpoint(intent.Destination, "destination")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fx, err := mapFXIntent(intent.FX)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &orchestratorv1.PaymentIntent{
|
||||
Kind: orchestratorv1.PaymentKind(intent.Kind),
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
Amount: mapMoney(intent.Amount),
|
||||
RequiresFx: intent.RequiresFX,
|
||||
Fx: fx,
|
||||
FeePolicy: mapPolicyOverrides(intent.FeePolicy),
|
||||
Attributes: copyStringMap(intent.Attributes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapPaymentEndpoint(endpoint *srequest.PaymentEndpoint, field string) (*orchestratorv1.PaymentEndpoint, error) {
|
||||
if endpoint == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
count int
|
||||
result orchestratorv1.PaymentEndpoint
|
||||
)
|
||||
|
||||
if endpoint.Ledger != nil {
|
||||
count++
|
||||
result.Endpoint = &orchestratorv1.PaymentEndpoint_Ledger{
|
||||
Ledger: mapLedgerEndpoint(endpoint.Ledger),
|
||||
}
|
||||
}
|
||||
if endpoint.ManagedWallet != nil {
|
||||
count++
|
||||
result.Endpoint = &orchestratorv1.PaymentEndpoint_ManagedWallet{
|
||||
ManagedWallet: mapManagedWalletEndpoint(endpoint.ManagedWallet),
|
||||
}
|
||||
}
|
||||
if endpoint.ExternalChain != nil {
|
||||
count++
|
||||
result.Endpoint = &orchestratorv1.PaymentEndpoint_ExternalChain{
|
||||
ExternalChain: mapExternalChainEndpoint(endpoint.ExternalChain),
|
||||
}
|
||||
}
|
||||
|
||||
if count > 1 {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint must set only one of ledger, managed_wallet, external_chain")
|
||||
}
|
||||
|
||||
result.Metadata = copyStringMap(endpoint.Metadata)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func mapLedgerEndpoint(endpoint *srequest.LedgerEndpoint) *orchestratorv1.LedgerEndpoint {
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
}
|
||||
return &orchestratorv1.LedgerEndpoint{
|
||||
LedgerAccountRef: endpoint.LedgerAccountRef,
|
||||
ContraLedgerAccountRef: endpoint.ContraLedgerAccountRef,
|
||||
}
|
||||
}
|
||||
|
||||
func mapManagedWalletEndpoint(endpoint *srequest.ManagedWalletEndpoint) *orchestratorv1.ManagedWalletEndpoint {
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
}
|
||||
return &orchestratorv1.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: endpoint.ManagedWalletRef,
|
||||
Asset: mapAsset(endpoint.Asset),
|
||||
}
|
||||
}
|
||||
|
||||
func mapExternalChainEndpoint(endpoint *srequest.ExternalChainEndpoint) *orchestratorv1.ExternalChainEndpoint {
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
}
|
||||
return &orchestratorv1.ExternalChainEndpoint{
|
||||
Asset: mapAsset(endpoint.Asset),
|
||||
Address: endpoint.Address,
|
||||
Memo: endpoint.Memo,
|
||||
}
|
||||
}
|
||||
|
||||
func mapAsset(asset *srequest.Asset) *chainv1.Asset {
|
||||
if asset == nil {
|
||||
return nil
|
||||
}
|
||||
return &chainv1.Asset{
|
||||
Chain: chainv1.ChainNetwork(asset.Chain),
|
||||
TokenSymbol: asset.TokenSymbol,
|
||||
ContractAddress: asset.ContractAddress,
|
||||
}
|
||||
}
|
||||
|
||||
func mapMoney(m *srequest.Money) *moneyv1.Money {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{
|
||||
Amount: m.Amount,
|
||||
Currency: m.Currency,
|
||||
}
|
||||
}
|
||||
|
||||
func mapFXIntent(fx *srequest.FXIntent) (*orchestratorv1.FXIntent, error) {
|
||||
if fx == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &orchestratorv1.FXIntent{
|
||||
Pair: mapCurrencyPair(fx.Pair),
|
||||
Side: fxv1.Side(fx.Side),
|
||||
Firm: fx.Firm,
|
||||
TtlMs: fx.TTLms,
|
||||
PreferredProvider: fx.PreferredProvider,
|
||||
MaxAgeMs: fx.MaxAgeMs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapCurrencyPair(pair *srequest.CurrencyPair) *fxv1.CurrencyPair {
|
||||
if pair == nil {
|
||||
return nil
|
||||
}
|
||||
return &fxv1.CurrencyPair{
|
||||
Base: pair.Base,
|
||||
Quote: pair.Quote,
|
||||
}
|
||||
}
|
||||
|
||||
func mapPolicyOverrides(policy *srequest.PolicyOverrides) *feesv1.PolicyOverrides {
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
return &feesv1.PolicyOverrides{
|
||||
InsufficientNet: feesv1.InsufficientNetPolicy(policy.InsufficientNet),
|
||||
}
|
||||
}
|
||||
|
||||
func copyStringMap(src map[string]string) map[string]string {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dst := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
@@ -46,12 +46,17 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
|
||||
payload.QuoteRef = ""
|
||||
}
|
||||
|
||||
intent, err := mapPaymentIntent(payload.Intent)
|
||||
if err != nil {
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
req := &orchestratorv1.InitiatePaymentRequest{
|
||||
Meta: &orchestratorv1.RequestMeta{
|
||||
OrganizationRef: orgRef.Hex(),
|
||||
},
|
||||
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
|
||||
Intent: payload.Intent,
|
||||
Intent: intent,
|
||||
FeeQuoteToken: strings.TrimSpace(payload.FeeQuoteToken),
|
||||
FxQuoteRef: strings.TrimSpace(payload.FxQuoteRef),
|
||||
QuoteRef: strings.TrimSpace(payload.QuoteRef),
|
||||
|
||||
@@ -39,12 +39,17 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
intent, err := mapPaymentIntent(payload.Intent)
|
||||
if err != nil {
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
req := &orchestratorv1.QuotePaymentRequest{
|
||||
Meta: &orchestratorv1.RequestMeta{
|
||||
OrganizationRef: orgRef.Hex(),
|
||||
},
|
||||
IdempotencyKey: payload.IdempotencyKey,
|
||||
Intent: payload.Intent,
|
||||
Intent: intent,
|
||||
PreviewOnly: payload.PreviewOnly,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ class CommonConstants {
|
||||
static String apiEndpoint = '/api/v1';
|
||||
static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79';
|
||||
static String amplitudeServerZone = 'EU';
|
||||
static String posthogApiKey = '';
|
||||
static String posthogHost = 'https://eu.i.posthog.com';
|
||||
static Locale defaultLocale = const Locale('en');
|
||||
static String defaultCurrency = 'EUR';
|
||||
static int defaultDimensionLength = 500;
|
||||
@@ -36,6 +38,8 @@ class CommonConstants {
|
||||
apiEndpoint = configJson['apiEndpoint'] ?? apiEndpoint;
|
||||
amplitudeSecret = configJson['amplitudeSecret'] ?? amplitudeSecret;
|
||||
amplitudeServerZone = configJson['amplitudeServerZone'] ?? amplitudeServerZone;
|
||||
posthogApiKey = configJson['posthogApiKey'] ?? posthogApiKey;
|
||||
posthogHost = configJson['posthogHost'] ?? posthogHost;
|
||||
defaultLocale = Locale(configJson['defaultLocale'] ?? defaultLocale.languageCode);
|
||||
defaultCurrency = configJson['defaultCurrency'] ?? defaultCurrency;
|
||||
wsProto = configJson['wsProto'] ?? wsProto;
|
||||
|
||||
@@ -15,6 +15,8 @@ class Constants extends CommonConstants {
|
||||
static String get currentOrgKey => CommonConstants.currentOrgKey;
|
||||
static String get apiUrl => CommonConstants.apiUrl;
|
||||
static String get serviceUrl => CommonConstants.serviceUrl;
|
||||
static String get posthogApiKey => CommonConstants.posthogApiKey;
|
||||
static String get posthogHost => CommonConstants.posthogHost;
|
||||
static int get defaultDimensionLength => CommonConstants.defaultDimensionLength;
|
||||
static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey;
|
||||
static String get nilObjectRef => CommonConstants.nilObjectRef;
|
||||
|
||||
@@ -21,6 +21,8 @@ extension AppConfigExtension on AppConfig {
|
||||
external String? get apiEndpoint;
|
||||
external String? get amplitudeSecret;
|
||||
external String? get amplitudeServerZone;
|
||||
external String? get posthogApiKey;
|
||||
external String? get posthogHost;
|
||||
external String? get defaultLocale;
|
||||
external String? get wsProto;
|
||||
external String? get wsEndpoint;
|
||||
@@ -40,6 +42,8 @@ class Constants extends CommonConstants {
|
||||
static String get currentOrgKey => CommonConstants.currentOrgKey;
|
||||
static String get apiUrl => CommonConstants.apiUrl;
|
||||
static String get serviceUrl => CommonConstants.serviceUrl;
|
||||
static String get posthogApiKey => CommonConstants.posthogApiKey;
|
||||
static String get posthogHost => CommonConstants.posthogHost;
|
||||
static int get defaultDimensionLength => CommonConstants.defaultDimensionLength;
|
||||
static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey;
|
||||
static String get nilObjectRef => CommonConstants.nilObjectRef;
|
||||
@@ -57,6 +61,8 @@ class Constants extends CommonConstants {
|
||||
'apiEndpoint': config.apiEndpoint,
|
||||
'amplitudeSecret': config.amplitudeSecret,
|
||||
'amplitudeServerZone': config.amplitudeServerZone,
|
||||
'posthogApiKey': config.posthogApiKey,
|
||||
'posthogHost': config.posthogHost,
|
||||
'defaultLocale': config.defaultLocale,
|
||||
'wsProto': config.wsProto,
|
||||
'wsEndpoint': config.wsEndpoint,
|
||||
|
||||
@@ -27,6 +27,7 @@ class AccountProvider extends ChangeNotifier {
|
||||
Resource<Account?> get resource => _resource;
|
||||
late LocaleProvider _localeProvider;
|
||||
PendingLogin? _pendingLogin;
|
||||
Future<void>? _restoreFuture;
|
||||
|
||||
Account? get account => _resource.data;
|
||||
PendingLogin? get pendingLogin => _pendingLogin;
|
||||
@@ -34,6 +35,7 @@ class AccountProvider extends ChangeNotifier {
|
||||
bool get isLoading => _resource.isLoading;
|
||||
Object? get error => _resource.error;
|
||||
bool get isReady => (!isLoading) && (account != null);
|
||||
Future<void>? get restoreFuture => _restoreFuture;
|
||||
|
||||
Account? currentUser() {
|
||||
final acc = account;
|
||||
@@ -220,4 +222,12 @@ class AccountProvider extends ChangeNotifier {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> restoreIfPossible() {
|
||||
return _restoreFuture ??= () async {
|
||||
final hasAuth = await AuthorizationService.isAuthorizationStored();
|
||||
if (!hasAuth) return;
|
||||
await restore();
|
||||
}();
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,9 @@ import 'package:pweb/providers/two_factor.dart';
|
||||
import 'package:pweb/providers/upload_history.dart';
|
||||
import 'package:pweb/providers/wallets.dart';
|
||||
import 'package:pweb/providers/wallet_transactions.dart';
|
||||
// import 'package:pweb/services/amplitude.dart';
|
||||
import 'package:pweb/services/operations.dart';
|
||||
import 'package:pweb/services/payments/history.dart';
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
import 'package:pweb/services/wallet_transactions.dart';
|
||||
import 'package:pweb/services/wallets.dart';
|
||||
|
||||
@@ -40,11 +40,9 @@ void _setupLogging() {
|
||||
}
|
||||
|
||||
void main() async {
|
||||
await Constants.initialize();
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// await AmplitudeService.initialize();
|
||||
await Constants.initialize();
|
||||
await PosthogService.initialize();
|
||||
|
||||
|
||||
_setupLogging();
|
||||
|
||||
@@ -14,7 +14,7 @@ import 'package:pshared/provider/recipient/pmethods.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/form/view.dart';
|
||||
// import 'package:pweb/services/amplitude.dart';
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
import 'package:pweb/utils/error/snackbar.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
import 'package:pweb/utils/snackbar.dart';
|
||||
@@ -105,11 +105,11 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
|
||||
return;
|
||||
}
|
||||
|
||||
// AmplitudeService.recipientAddCompleted(
|
||||
// _type,
|
||||
// _status,
|
||||
// _methods.keys.toSet(),
|
||||
// );
|
||||
await PosthogService.recipientAddCompleted(
|
||||
_type,
|
||||
_status,
|
||||
_methods.keys.toSet(),
|
||||
);
|
||||
final recipient = await executeActionWithNotification(
|
||||
context: context,
|
||||
action: _doSave,
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:pshared/provider/account.dart';
|
||||
|
||||
import 'package:pweb/app/router/pages.dart';
|
||||
import 'package:pweb/widgets/error/snackbar.dart';
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -17,15 +18,38 @@ class AccountLoader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Consumer<AccountProvider>(builder: (context, provider, _) {
|
||||
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
|
||||
if (provider.error != null) {
|
||||
postNotifyUserOfErrorX(
|
||||
context: context,
|
||||
errorSituation: AppLocalizations.of(context)!.errorLogin,
|
||||
exception: provider.error!,
|
||||
);
|
||||
navigateAndReplace(context, Pages.login);
|
||||
if (provider.account != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final account = provider.account;
|
||||
if (account != null) {
|
||||
PosthogService.identify(account);
|
||||
}
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
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) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login));
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:pweb/widgets/password/password.dart';
|
||||
import 'package:pweb/widgets/username.dart';
|
||||
import 'package:pweb/widgets/vspacer.dart';
|
||||
import 'package:pweb/widgets/error/snackbar.dart';
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -43,10 +44,14 @@ class _LoginFormState extends State<LoginForm> {
|
||||
password: _passwordController.text,
|
||||
locale: context.read<LocaleProvider>().locale.languageCode,
|
||||
);
|
||||
await PosthogService.login(pending: outcome.isPending);
|
||||
if (outcome.isPending) {
|
||||
// TODO: fix context usage
|
||||
navigateAndReplace(context, Pages.sfactor);
|
||||
} else {
|
||||
if (outcome.account != null) {
|
||||
await PosthogService.identify(outcome.account!);
|
||||
}
|
||||
onLogin();
|
||||
}
|
||||
return 'ok';
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:pweb/providers/payment_flow.dart';
|
||||
import 'package:pweb/pages/payment_methods/payment_page/body.dart';
|
||||
import 'package:pweb/providers/wallets.dart';
|
||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
|
||||
|
||||
class PaymentPage extends StatefulWidget {
|
||||
@@ -109,7 +110,7 @@ class _PaymentPageState extends State<PaymentPage> {
|
||||
|
||||
void _handleSendPayment() {
|
||||
// TODO: Handle Payment logic
|
||||
// AmplitudeService.paymentInitiated();
|
||||
PosthogService.paymentInitiated(method: _flowProvider.selectedType);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/locale.dart';
|
||||
|
||||
// import 'package:pweb/services/amplitude.dart';
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -58,7 +58,7 @@ class LocalePicker extends StatelessWidget {
|
||||
onChanged: (locale) {
|
||||
if (locale != null) {
|
||||
localeProvider.setLocale(locale);
|
||||
// AmplitudeService.localeChanged(locale);
|
||||
PosthogService.localeChanged(locale);
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
|
||||
136
frontend/pweb/lib/services/posthog.dart
Normal file
136
frontend/pweb/lib/services/posthog.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'package:posthog_flutter/posthog_flutter.dart';
|
||||
|
||||
import 'package:pshared/config/constants.dart';
|
||||
import 'package:pshared/models/account/account.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
import 'package:pshared/models/recipient/type.dart';
|
||||
|
||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||
|
||||
|
||||
class PosthogService {
|
||||
static final _logger = Logger('service.posthog');
|
||||
static String? _identifiedUserId;
|
||||
static bool _initialized = false;
|
||||
|
||||
static bool get isEnabled => _initialized;
|
||||
|
||||
static Future<void> initialize() async {
|
||||
final apiKey = Constants.posthogApiKey;
|
||||
if (apiKey.isEmpty) {
|
||||
_logger.warning('PostHog API key is not configured, analytics disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final config = PostHogConfig(apiKey)
|
||||
..host = Constants.posthogHost
|
||||
..captureApplicationLifecycleEvents = true;
|
||||
await Posthog().setup(config);
|
||||
await Posthog().register('client_id', Constants.clientId);
|
||||
_initialized = true;
|
||||
_logger.info('PostHog initialized with host ${Constants.posthogHost}');
|
||||
} catch (e, st) {
|
||||
_initialized = false;
|
||||
_logger.warning('Failed to initialize PostHog: $e', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> identify(Account account) async {
|
||||
if (!_initialized) return;
|
||||
if (_identifiedUserId == account.id) return;
|
||||
|
||||
await Posthog().identify(
|
||||
userId: account.id,
|
||||
userProperties: {
|
||||
'email': account.login,
|
||||
'name': account.name,
|
||||
'locale': account.locale,
|
||||
'created_at': account.createdAt.toIso8601String(),
|
||||
},
|
||||
);
|
||||
_identifiedUserId = account.id;
|
||||
}
|
||||
|
||||
static Future<void> reset() async {
|
||||
if (!_initialized) return;
|
||||
_identifiedUserId = null;
|
||||
await Posthog().reset();
|
||||
}
|
||||
|
||||
static Future<void> login({required bool pending}) async {
|
||||
if (!_initialized) return;
|
||||
await _capture(
|
||||
'login',
|
||||
properties: {
|
||||
'result': pending ? 'pending' : 'success',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> pageOpened(PayoutDestination page, {String? path, String? uiSource}) async {
|
||||
if (!_initialized) return;
|
||||
return _capture(
|
||||
'pageOpened',
|
||||
properties: {
|
||||
'page': page.name,
|
||||
if (path != null) 'path': path,
|
||||
if (uiSource != null) 'uiSource': uiSource,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> localeChanged(Locale locale) async {
|
||||
if (!_initialized) return;
|
||||
return _capture(
|
||||
'localeChanged',
|
||||
properties: {'locale': locale.toLanguageTag()},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> recipientAddCompleted(
|
||||
RecipientType type,
|
||||
RecipientStatus status,
|
||||
Set<PaymentType> methods,
|
||||
) async {
|
||||
if (!_initialized) return;
|
||||
return _capture(
|
||||
'recipientAddCompleted',
|
||||
properties: {
|
||||
'methods': methods.map((m) => m.name).toList(),
|
||||
'type': type.name,
|
||||
'status': status.name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> paymentInitiated({PaymentType? method}) async {
|
||||
if (!_initialized) return;
|
||||
return _capture(
|
||||
'paymentInitiated',
|
||||
properties: {
|
||||
if (method != null) 'method': method.name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _capture(
|
||||
String eventName, {
|
||||
Map<String, Object?>? properties,
|
||||
}) async {
|
||||
if (!_initialized) return;
|
||||
final filtered = <String, Object>{};
|
||||
if (properties != null) {
|
||||
for (final entry in properties.entries) {
|
||||
final value = entry.value;
|
||||
if (value != null) filtered[entry.key] = value;
|
||||
}
|
||||
}
|
||||
await Posthog().capture(eventName: eventName, properties: filtered.isEmpty ? null : filtered);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/provider/permissions.dart';
|
||||
|
||||
import 'package:pweb/app/router/pages.dart';
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
|
||||
|
||||
void logoutUtil(BuildContext context) {
|
||||
context.read<AccountProvider>().logout();
|
||||
context.read<PermissionsProvider>().reset();
|
||||
PosthogService.reset();
|
||||
navigateAndReplace(context, Pages.login);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// import 'package:pweb/services/amplitude.dart';
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class SideMenuColumn extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
onSelected(item);
|
||||
// AmplitudeService.pageOpened(item, uiSource: 'sidebar');
|
||||
PosthogService.pageOpened(item, uiSource: 'sidebar');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
hoverColor: theme.colorScheme.primaryContainer,
|
||||
|
||||
@@ -9,6 +9,7 @@ import amplitude_flutter
|
||||
import file_selector_macos
|
||||
import flutter_timezone
|
||||
import path_provider_foundation
|
||||
import posthog_flutter
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
@@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
PosthogFlutterPlugin.register(with: registry.registrar(forPlugin: "PosthogFlutterPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
|
||||
@@ -35,6 +35,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
pshared:
|
||||
path: ../pshared
|
||||
posthog_flutter: ^4.0.1
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
||||
Reference in New Issue
Block a user