Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- feat: Add `onFeatureFlags` callback to `PostHogConfig` to get notified when feature flags are loaded. Use `Posthog().getFeatureFlag()` or `Posthog().isFeatureEnabled()` within the callback to access fresh flag values. ([#224](https://github.com/PostHog/posthog-flutter/pull/224))

## 5.9.1

- fix: TextFormField widgets were not being masked ([#227](https://github.com/PostHog/posthog-flutter/pull/227))
Expand Down
4 changes: 3 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ include: package:flutter_lints/flutter.yaml

linter:
rules:
unnecessary_library_name: false
unnecessary_library_name: false
avoid_annotating_with_dynamic: true
avoid_dynamic_calls: true
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.util.Log
import com.posthog.PersonProfiles
import com.posthog.PostHog
import com.posthog.PostHogConfig
import com.posthog.PostHogOnFeatureFlags
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
import com.posthog.android.internal.getApplicationInfo
Expand Down Expand Up @@ -305,7 +306,14 @@ class PosthogFlutterPlugin :

sdkName = "posthog-flutter"
sdkVersion = postHogVersion

onFeatureFlags =
PostHogOnFeatureFlags {
Log.i("PostHogFlutter", "Android onFeatureFlags triggered. Notifying Dart.")
invokeFlutterMethod("onFeatureFlagsCallback", emptyMap<String, Any?>())
}
}

PostHogAndroid.setup(applicationContext, config)
}

Expand Down
3 changes: 3 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final config =
PostHogConfig('phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D');
config.onFeatureFlags = () {
debugPrint('[PostHog] Feature flags loaded!');
};
config.debug = true;
config.captureApplicationLifecycleEvents = false;
config.host = 'https://us.i.posthog.com';
Expand Down
30 changes: 24 additions & 6 deletions ios/Classes/PosthogFlutterPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,47 @@ import PostHog

public class PosthogFlutterPlugin: NSObject, FlutterPlugin {
private static var instance: PosthogFlutterPlugin?
private var channel: FlutterMethodChannel?

public static func getInstance() -> PosthogFlutterPlugin? {
instance
}

override init() {
super.init()
NotificationCenter.default.addObserver(
self,
selector: #selector(featureFlagsDidUpdate),
name: PostHogSDK.didReceiveFeatureFlags,
object: nil
)
}

deinit {
NotificationCenter.default.removeObserver(self)
}

public static func register(with registrar: FlutterPluginRegistrar) {
let methodChannel: FlutterMethodChannel
#if os(iOS)
let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger())
methodChannel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger())
#elseif os(macOS)
let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger)
methodChannel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger)
#endif
let instance = PosthogFlutterPlugin()
instance.channel = channel
instance.channel = methodChannel
PosthogFlutterPlugin.instance = instance
initPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addMethodCallDelegate(instance, channel: methodChannel)
}

@objc func featureFlagsDidUpdate() {
invokeFlutterMethod("onFeatureFlagsCallback", arguments: [String: Any]())
}

private let dispatchQueue = DispatchQueue(label: "com.posthog.PosthogFlutterPlugin",
target: .global(qos: .utility))

private var channel: FlutterMethodChannel?

public static func initPlugin() {
let autoInit = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.AUTO_INIT") as? Bool ?? true
if !autoInit {
Expand Down
209 changes: 205 additions & 4 deletions lib/posthog_flutter_web.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// In order to *not* need this ignore, consider extracting the "web" version
// of your plugin as a separate package, instead of inlining it in the same
// package as the core of your plugin.
import 'dart:js_interop';

import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';

import 'src/posthog_config.dart';
import 'src/posthog_flutter_platform_interface.dart';
import 'src/posthog_flutter_web_handler.dart';

Expand All @@ -13,15 +16,213 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
PosthogFlutterWeb();

static void registerWith(Registrar registrar) {
final MethodChannel channel = MethodChannel(
final channel = MethodChannel(
'posthog_flutter',
const StandardMethodCodec(),
registrar,
);
final PosthogFlutterWeb instance = PosthogFlutterWeb();
final instance = PosthogFlutterWeb();
channel.setMethodCallHandler(instance.handleMethodCall);

// Set the platform instance so that Posthog() can call methods directly
// on the web implementation instead of going through the method channel.
// This is required because method channels can only pass serializable data,
// not function callbacks like onFeatureFlags.
PosthogFlutterPlatformInterface.instance = instance;
}

Future<dynamic> handleMethodCall(MethodCall call) async {
// The 'setup' call is now handled by the setup method override.
// Other method calls are delegated to handleWebMethodCall.
if (call.method == 'setup') {
// This case should ideally not be hit if Posthog().setup directly calls the overridden setup.
// However, to be safe, we can log or ignore.
// For now, let's assume direct call to overridden setup handles it.
return null;
}
return handleWebMethodCall(call);
}

@override
Future<void> setup(PostHogConfig config) async {
// It's assumed posthog-js is initialized by the user in their HTML.
// This setup primarily hooks into the existing posthog-js instance.

// If apiKey and host are in config, and posthog.init is to be handled by plugin:
// This is an example if we wanted the plugin to also call posthog.init()
// final jsOptions = <String, dynamic>{
// 'api_host': config.host,
// // Add other relevant options from PostHogConfig if needed for JS init
// }.jsify();
// posthog?.callMethod('init'.toJS, config.apiKey.toJS, jsOptions);

final ph = posthog;
if (config.onFeatureFlags != null && ph != null) {
final dartCallback = config.onFeatureFlags!;

// JS SDK calls with: (flags: string[], variants: Record, context?: {errorsLoading})
// We ignore the JS parameters and just invoke the void callback
final jsCallback =
(JSArray jsFlags, JSObject jsFlagVariants, [JSObject? jsContext]) {
dartCallback();
}.toJS;

ph.onFeatureFlags(jsCallback);
}
}

@override
Future<void> identify({
required String userId,
Map<String, Object>? userProperties,
Map<String, Object>? userPropertiesSetOnce,
}) async {
return handleWebMethodCall(MethodCall('identify', {
'userId': userId,
if (userProperties != null) 'userProperties': userProperties,
if (userPropertiesSetOnce != null)
'userPropertiesSetOnce': userPropertiesSetOnce,
}));
}

@override
Future<void> capture({
required String eventName,
Map<String, Object>? properties,
}) async {
return handleWebMethodCall(MethodCall('capture', {
'eventName': eventName,
if (properties != null) 'properties': properties,
}));
}

@override
Future<void> screen({
required String screenName,
Map<String, Object>? properties,
}) async {
return handleWebMethodCall(MethodCall('screen', {
'screenName': screenName,
if (properties != null) 'properties': properties,
}));
}

@override
Future<void> alias({required String alias}) async {
return handleWebMethodCall(MethodCall('alias', {'alias': alias}));
}

@override
Future<String> getDistinctId() async {
final result = await handleWebMethodCall(const MethodCall('distinctId'));
return result as String? ?? '';
}

@override
Future<void> reset() async {
return handleWebMethodCall(const MethodCall('reset'));
}

@override
Future<void> disable() async {
return handleWebMethodCall(const MethodCall('disable'));
}

@override
Future<void> enable() async {
return handleWebMethodCall(const MethodCall('enable'));
}

@override
Future<bool> isOptOut() async {
final result = await handleWebMethodCall(const MethodCall('isOptOut'));
return result as bool? ?? true;
}

Future<dynamic> handleMethodCall(MethodCall call) =>
handleWebMethodCall(call);
@override
Future<void> debug(bool enabled) async {
return handleWebMethodCall(MethodCall('debug', {'debug': enabled}));
}

@override
Future<void> register(String key, Object value) async {
return handleWebMethodCall(
MethodCall('register', {'key': key, 'value': value}));
}

@override
Future<void> unregister(String key) async {
return handleWebMethodCall(MethodCall('unregister', {'key': key}));
}

@override
Future<bool> isFeatureEnabled(String key) async {
final result =
await handleWebMethodCall(MethodCall('isFeatureEnabled', {'key': key}));
return result as bool? ?? false;
}

@override
Future<void> reloadFeatureFlags() async {
return handleWebMethodCall(const MethodCall('reloadFeatureFlags'));
}

@override
Future<void> group({
required String groupType,
required String groupKey,
Map<String, Object>? groupProperties,
}) async {
return handleWebMethodCall(MethodCall('group', {
'groupType': groupType,
'groupKey': groupKey,
if (groupProperties != null) 'groupProperties': groupProperties,
}));
}

@override
Future<Object?> getFeatureFlag({required String key}) async {
return handleWebMethodCall(MethodCall('getFeatureFlag', {'key': key}));
}

@override
Future<Object?> getFeatureFlagPayload({required String key}) async {
return handleWebMethodCall(
MethodCall('getFeatureFlagPayload', {'key': key}));
}

@override
Future<void> flush() async {
return handleWebMethodCall(const MethodCall('flush'));
}

@override
Future<void> close() async {
return handleWebMethodCall(const MethodCall('close'));
}

@override
Future<String?> getSessionId() async {
final result = await handleWebMethodCall(const MethodCall('getSessionId'));
return result as String?;
}

@override
Future<void> openUrl(String url) async {
// Not supported on web
}

@override
Future<void> showSurvey(Map<String, dynamic> survey) async {
// Not supported on web - surveys handled by posthog-js
}

@override
Future<void> captureException({
required Object error,
StackTrace? stackTrace,
Map<String, Object>? properties,
}) async {
// Not implemented on web
}
}
2 changes: 2 additions & 0 deletions lib/src/error_tracking/isolate_handler_web.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ignore_for_file: avoid_annotating_with_dynamic

/// Web platform stub implementation of isolate error handling
/// Isolates are not available on web, so this is a no-op implementation
class IsolateErrorHandler {
Expand Down
23 changes: 19 additions & 4 deletions lib/src/posthog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,25 @@ class Posthog {

String? _currentScreen;

/// Android and iOS only
/// Only used for the manual setup
/// Requires disabling the automatic init on Android and iOS:
/// com.posthog.posthog.AUTO_INIT: false
/// Initializes the PostHog SDK.
///
/// This method sets up the connection to your PostHog instance and prepares the SDK for tracking events and feature flags.
///
/// - [config]: The [PostHogConfig] object containing your API key, host, and other settings.
/// To listen for feature flag load events, provide an `onFeatureFlags` callback in the [PostHogConfig].
///
/// **Example:**
/// ```dart
/// final config = PostHogConfig('YOUR_API_KEY');
/// config.host = 'YOUR_POSTHOG_HOST';
/// config.onFeatureFlags = () {
/// // Feature flags are now loaded, you can read flag values here
/// };
/// await Posthog().setup(config);
/// ```
///
/// For Android and iOS, if you are performing a manual setup,
/// ensure `com.posthog.posthog.AUTO_INIT: false` is set in your native configuration.
Future<void> setup(PostHogConfig config) {
_config = config; // Store the config

Expand Down
16 changes: 13 additions & 3 deletions lib/src/posthog_config.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'posthog_flutter_platform_interface.dart';

enum PostHogPersonProfiles { never, always, identifiedOnly }

enum PostHogDataMode { wifi, cellular, any }
Expand Down Expand Up @@ -44,10 +46,18 @@ class PostHogConfig {
/// Configuration for error tracking and exception capture
final errorTrackingConfig = PostHogErrorTrackingConfig();

// TODO: missing getAnonymousId, propertiesSanitizer, captureDeepLinks
// onFeatureFlags, integrations
/// Callback to be invoked when feature flags are loaded.
///
/// Use [Posthog.getFeatureFlag] or [Posthog.isFeatureEnabled] within this
/// callback to access the loaded flag values.
OnFeatureFlagsCallback? onFeatureFlags;

// TODO: missing getAnonymousId, propertiesSanitizer, captureDeepLinks integrations

PostHogConfig(this.apiKey);
PostHogConfig(
this.apiKey, {
this.onFeatureFlags,
});

Map<String, dynamic> toMap() {
return {
Expand Down
Loading
Loading