diff --git a/frontend/pshared/lib/config/common.dart b/frontend/pshared/lib/config/common.dart index c2240f9..8f2702b 100644 --- a/frontend/pshared/lib/config/common.dart +++ b/frontend/pshared/lib/config/common.dart @@ -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; diff --git a/frontend/pshared/lib/config/mobile.dart b/frontend/pshared/lib/config/mobile.dart index ec5f56d..d340010 100644 --- a/frontend/pshared/lib/config/mobile.dart +++ b/frontend/pshared/lib/config/mobile.dart @@ -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; diff --git a/frontend/pshared/lib/config/web.dart b/frontend/pshared/lib/config/web.dart index cc49d85..8339cf5 100644 --- a/frontend/pshared/lib/config/web.dart +++ b/frontend/pshared/lib/config/web.dart @@ -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, diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart index 9413913..b6239db 100644 --- a/frontend/pshared/lib/provider/account.dart +++ b/frontend/pshared/lib/provider/account.dart @@ -27,6 +27,7 @@ class AccountProvider extends ChangeNotifier { Resource get resource => _resource; late LocaleProvider _localeProvider; PendingLogin? _pendingLogin; + Future? _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? get restoreFuture => _restoreFuture; Account? currentUser() { final acc = account; @@ -220,4 +222,12 @@ class AccountProvider extends ChangeNotifier { rethrow; } } -} + + Future restoreIfPossible() { + return _restoreFuture ??= () async { + final hasAuth = await AuthorizationService.isAuthorizationStored(); + if (!hasAuth) return; + await restore(); + }(); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 1e2715b..ee08d0b 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -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(); diff --git a/frontend/pweb/lib/pages/address_book/form/page.dart b/frontend/pweb/lib/pages/address_book/form/page.dart index 1568be3..df5e8f0 100644 --- a/frontend/pweb/lib/pages/address_book/form/page.dart +++ b/frontend/pweb/lib/pages/address_book/form/page.dart @@ -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 { return; } - // AmplitudeService.recipientAddCompleted( - // _type, - // _status, - // _methods.keys.toSet(), - // ); + await PosthogService.recipientAddCompleted( + _type, + _status, + _methods.keys.toSet(), + ); final recipient = await executeActionWithNotification( context: context, action: _doSave, diff --git a/frontend/pweb/lib/pages/loaders/account.dart b/frontend/pweb/lib/pages/loaders/account.dart index d891e1c..9b78e25 100644 --- a/frontend/pweb/lib/pages/loaders/account.dart +++ b/frontend/pweb/lib/pages/loaders/account.dart @@ -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(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()); diff --git a/frontend/pweb/lib/pages/login/form.dart b/frontend/pweb/lib/pages/login/form.dart index 1eb3bcc..1786ef9 100644 --- a/frontend/pweb/lib/pages/login/form.dart +++ b/frontend/pweb/lib/pages/login/form.dart @@ -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 { password: _passwordController.text, locale: context.read().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'; diff --git a/frontend/pweb/lib/pages/payment_methods/page.dart b/frontend/pweb/lib/pages/payment_methods/page.dart index 44d3dd9..ccf4b7a 100644 --- a/frontend/pweb/lib/pages/payment_methods/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/page.dart @@ -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 { void _handleSendPayment() { // TODO: Handle Payment logic - // AmplitudeService.paymentInitiated(); + PosthogService.paymentInitiated(method: _flowProvider.selectedType); } @override @@ -195,4 +196,4 @@ class _PaymentPageState extends State { (method.description?.contains(wallet.walletUserID) ?? false), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/settings/profile/account/locale.dart b/frontend/pweb/lib/pages/settings/profile/account/locale.dart index 4c11cc5..2208197 100644 --- a/frontend/pweb/lib/pages/settings/profile/account/locale.dart +++ b/frontend/pweb/lib/pages/settings/profile/account/locale.dart @@ -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( diff --git a/frontend/pweb/lib/services/posthog.dart b/frontend/pweb/lib/services/posthog.dart new file mode 100644 index 0000000..15ceaf2 --- /dev/null +++ b/frontend/pweb/lib/services/posthog.dart @@ -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 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 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 reset() async { + if (!_initialized) return; + _identifiedUserId = null; + await Posthog().reset(); + } + + static Future login({required bool pending}) async { + if (!_initialized) return; + await _capture( + 'login', + properties: { + 'result': pending ? 'pending' : 'success', + }, + ); + } + + static Future 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 localeChanged(Locale locale) async { + if (!_initialized) return; + return _capture( + 'localeChanged', + properties: {'locale': locale.toLanguageTag()}, + ); + } + + static Future recipientAddCompleted( + RecipientType type, + RecipientStatus status, + Set methods, + ) async { + if (!_initialized) return; + return _capture( + 'recipientAddCompleted', + properties: { + 'methods': methods.map((m) => m.name).toList(), + 'type': type.name, + 'status': status.name, + }, + ); + } + + static Future paymentInitiated({PaymentType? method}) async { + if (!_initialized) return; + return _capture( + 'paymentInitiated', + properties: { + if (method != null) 'method': method.name, + }, + ); + } + + static Future _capture( + String eventName, { + Map? properties, + }) async { + if (!_initialized) return; + final filtered = {}; + 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); + } +} diff --git a/frontend/pweb/lib/utils/logout.dart b/frontend/pweb/lib/utils/logout.dart index 97e030e..6881022 100644 --- a/frontend/pweb/lib/utils/logout.dart +++ b/frontend/pweb/lib/utils/logout.dart @@ -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().logout(); context.read().reset(); + PosthogService.reset(); navigateAndReplace(context, Pages.login); } diff --git a/frontend/pweb/lib/widgets/sidebar/side_menu.dart b/frontend/pweb/lib/widgets/sidebar/side_menu.dart index 5e9cc13..026483a 100644 --- a/frontend/pweb/lib/widgets/sidebar/side_menu.dart +++ b/frontend/pweb/lib/widgets/sidebar/side_menu.dart @@ -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, @@ -76,4 +76,4 @@ class SideMenuColumn extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift index 79f5652..6f6fb74 100644 --- a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/frontend/pweb/pubspec.yaml b/frontend/pweb/pubspec.yaml index aeb7e35..00a60ba 100644 --- a/frontend/pweb/pubspec.yaml +++ b/frontend/pweb/pubspec.yaml @@ -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.