Skip to content
Open
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 `Posthog()` to listen for feature flag load events. On Web, this callback provides all flags and variants. On mobile (Android/iOS), it serves as a signal that flags have been loaded by the native SDK; the `flags` and `flagVariants` parameters will be empty in the callback, and developers should use `Posthog.getFeatureFlag()` or `Posthog.isFeatureEnabled()` to retrieve specific flag values. This allows developers to ensure flags are loaded before checking them, especially on the first app run. ([#183](https://github.com/PostHog/posthog-flutter/pull/183))

## 5.0.0

- chore: support flutter web wasm builds ([#112](https://github.com/PostHog/posthog-flutter/pull/112))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package com.posthog.flutter

import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.posthog.PersonProfiles
import com.posthog.PostHog
import com.posthog.PostHogConfig
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
import com.posthog.android.internal.getApplicationInfo
import com.posthog.PostHogOnFeatureFlags
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
Expand Down Expand Up @@ -262,7 +265,20 @@ class PosthogFlutterPlugin :

sdkName = "posthog-flutter"
sdkVersion = postHogVersion

onFeatureFlags = PostHogOnFeatureFlags {
try {
Log.i("PostHogFlutter", "Android onFeatureFlags triggered. Notifying Dart.")
val arguments = emptyMap<String, Any?>()
channel.invokeMethod("onFeatureFlagsCallback", arguments)
} catch (e: Exception) {
Log.e("PostHogFlutter", "Error in onFeatureFlags signalling: ${e.message}", e)
val errorArguments = emptyMap<String, Any?>()
channel.invokeMethod("onFeatureFlagsCallback", errorArguments)
}
}
}

PostHogAndroid.setup(applicationContext, config)
}

Expand Down
37 changes: 34 additions & 3 deletions ios/Classes/PosthogFlutterPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,46 @@ import PostHog
#endif

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

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

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 = methodChannel

initPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addMethodCallDelegate(instance, channel: methodChannel)
}

@objc func featureFlagsDidUpdate() {
let flags: [String] = []
let flagVariants: [String: Any] = [:]

guard let channel = self.channel else {
print("PosthogFlutterPlugin: FlutterMethodChannel is nil in featureFlagsDidUpdate.")
return
}

channel.invokeMethod("onFeatureFlagsCallback", arguments: [
"flags": flags,
"flagVariants": flagVariants,
"errorsLoading": false
])
}

private let dispatchQueue = DispatchQueue(label: "com.posthog.PosthogFlutterPlugin",
Expand Down
51 changes: 49 additions & 2 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 @@ -20,8 +23,52 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
);
final PosthogFlutterWeb instance = PosthogFlutterWeb();
channel.setMethodCallHandler(instance.handleMethodCall);
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);
}

Future<dynamic> handleMethodCall(MethodCall call) =>
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);


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

final jsCallback = (JSArray jsFlags, JSObject jsFlagVariants) {
final List<String> flags = jsFlags.toDart.whereType<String>().toList();

Map<String, dynamic> flagVariants = {};
final dartVariantsMap = jsFlagVariants.dartify() as Map<dynamic, dynamic>?;
if (dartVariantsMap != null) {
flagVariants = dartVariantsMap.map((key, value) => MapEntry(key.toString(), value));
}

// When posthog-js onFeatureFlags fires, it implies successful loading.
dartCallback(flags, flagVariants, errorsLoading: false);
}.toJS;

posthog!.onFeatureFlags(jsCallback);
}
}
}
46 changes: 42 additions & 4 deletions lib/src/posthog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,48 @@ 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].
///
/// **Behavior of `onFeatureFlags` callback (when provided in `PostHogConfig`):**
///
/// **Web:**
/// The callback will receive:
/// - `flags`: A list of active feature flag keys (List<String>).
/// - `flagVariants`: A map of feature flag keys to their variant values (Map<String, dynamic>).
/// - `errorsLoading`: Will be `false` as the callback firing implies success.
///
/// **Mobile (Android/iOS):**
/// The callback serves primarily as a notification that the native PostHog SDK
/// has finished loading feature flags. In this case:
/// - `flags`: Will be an empty list.
/// - `flagVariants`: Will be an empty map.
/// - `errorsLoading`: Will be `null` if the native call was successful but contained no error info, or `true` if an error occurred during Dart-side processing of the callback.
/// After this callback is invoked, you can reliably use `Posthog().getFeatureFlag('your-flag-key')`
/// or `Posthog().isFeatureEnabled('your-flag-key')` to get the values of specific flags.
///
/// **Example with `onFeatureFlags` in `PostHogConfig`:**
/// ```dart
/// final config = PostHogConfig(
/// apiKey: 'YOUR_API_KEY',
/// host: 'YOUR_POSTHOG_HOST',
/// onFeatureFlags: (flags, flagVariants, {errorsLoading}) {
/// if (errorsLoading == true) {
/// print('Error loading feature flags!');
/// return;
/// }
/// // ... process flags ...
/// },
/// );
/// 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
return _posthog.setup(config);
Expand Down
11 changes: 10 additions & 1 deletion 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 @@ -28,10 +30,17 @@ class PostHogConfig {
/// iOS only
var dataMode = PostHogDataMode.any;

/// Callback to be invoked when feature flags are loaded.
/// See [Posthog.onFeatureFlags] for more details on behavior per platform.
final OnFeatureFlagsCallback? onFeatureFlags;

// TODO: missing getAnonymousId, propertiesSanitizer, captureDeepLinks
// onFeatureFlags, integrations

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

Map<String, dynamic> toMap() {
return {
Expand Down
46 changes: 46 additions & 0 deletions lib/src/posthog_flutter_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,58 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
/// The method channel used to interact with the native platform.
final _methodChannel = const MethodChannel('posthog_flutter');

OnFeatureFlagsCallback? _onFeatureFlagsCallback;
bool _methodCallHandlerInitialized = false;

void _ensureMethodCallHandlerInitialized() {
if (!_methodCallHandlerInitialized) {
_methodChannel.setMethodCallHandler(_handleMethodCall);
_methodCallHandlerInitialized = true;
}
}

Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'onFeatureFlagsCallback':
if (_onFeatureFlagsCallback != null) {
try {
final args = call.arguments as Map<dynamic, dynamic>;
// Ensure correct types from native
// For mobile, args will be an empty map. Callback expects optional params.
final flags =
(args['flags'] as List<dynamic>?)?.cast<String>() ?? [];
final flagVariants =
(args['flagVariants'] as Map<dynamic, dynamic>?)
?.map((k, v) => MapEntry(k.toString(), v)) ??
<String, dynamic>{};
// For mobile, errorsLoading is not explicitly sent, so it will be null here.
final errorsLoading = args['errorsLoading'] as bool?;

_onFeatureFlagsCallback!(flags, flagVariants,
errorsLoading: errorsLoading);
} catch (e, s) {
printIfDebug('Error processing onFeatureFlagsCallback: $e\n$s');
_onFeatureFlagsCallback!([], <String, dynamic>{},
errorsLoading: true);
}
}
break;
default:
break;
}
}

@override
Future<void> setup(PostHogConfig config) async {
if (!isSupportedPlatform()) {
return;
}

_onFeatureFlagsCallback = config.onFeatureFlags;
if (_onFeatureFlagsCallback != null) {
_ensureMethodCallHandlerInitialized();
}

try {
await _methodChannel.invokeMethod('setup', config.toMap());
} on PlatformException catch (exception) {
Expand Down
8 changes: 8 additions & 0 deletions lib/src/posthog_flutter_platform_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'posthog_config.dart';
import 'posthog_flutter_io.dart';

/// Defines the callback signature for when feature flags are loaded.
/// [flags] is a list of active feature flag keys.
/// [flagVariants] is a map of feature flag keys to their variant values (String or bool).
/// [errorsLoading] is true if there was an error loading flags, otherwise false or null.
typedef OnFeatureFlagsCallback = void Function(
List<String> flags, Map<String, dynamic> flagVariants,
{bool? errorsLoading});

abstract class PosthogFlutterPlatformInterface extends PlatformInterface {
/// Constructs a PosthogFlutterPlatform.
PosthogFlutterPlatformInterface() : super(token: _token);
Expand Down
1 change: 1 addition & 0 deletions lib/src/posthog_flutter_web_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ extension PostHogExtension on PostHog {
external void register(JSAny properties);
external void unregister(JSAny key);
external JSAny? get_session_id();
external void onFeatureFlags(JSFunction callback);
}

// Accessing PostHog from the window object
Expand Down
Loading