Skip to content

Commit db5dc16

Browse files
ioannisjlaf-rge
andauthored
feat: add onFeatureFlags callback (#224)
* fix: code comments * feat: add onFeatureFlags callback # Conflicts: # CHANGELOG.md # ios/Classes/PosthogFlutterPlugin.swift # lib/src/posthog_flutter_io.dart # test/posthog_flutter_platform_interface_fake.dart * Update CHANGELOG.md Co-authored-by: Ioannis J <yiannis@posthog.com> * Reapply "fixing based on maintainer feedback" This reverts commit 91cf32f. # Conflicts: # lib/src/posthog_config.dart # lib/src/posthog_flutter_io.dart # test/posthog_flutter_platform_interface_fake.dart * removed main thread invocation * fix: remove ensureMethodCallHandlerInitialized * chore: format * fix: plugins * fix: config * chore: update CHANGELOG * fix: force unwrap * fix: add missing methods in PosthogFlutterWeb * fix: remove onFeatureFlags * fix: comments * fix: add lint rules * fix: type inference * feat: simplify flag callback * chore: update changelog * fix: changelog --------- Co-authored-by: William N. Green <william@swanbitcoin.com> Co-authored-by: William N. Green <william.green@gmail.com>
1 parent d7b2b71 commit db5dc16

18 files changed

+509
-20
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Next
22

3+
- 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))
4+
35
## 5.9.1
46

57
- fix: TextFormField widgets were not being masked ([#227](https://github.com/PostHog/posthog-flutter/pull/227))

analysis_options.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ include: package:flutter_lints/flutter.yaml
55

66
linter:
77
rules:
8-
unnecessary_library_name: false
8+
unnecessary_library_name: false
9+
avoid_annotating_with_dynamic: true
10+
avoid_dynamic_calls: true

android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.util.Log
1111
import com.posthog.PersonProfiles
1212
import com.posthog.PostHog
1313
import com.posthog.PostHogConfig
14+
import com.posthog.PostHogOnFeatureFlags
1415
import com.posthog.android.PostHogAndroid
1516
import com.posthog.android.PostHogAndroidConfig
1617
import com.posthog.android.internal.getApplicationInfo
@@ -305,7 +306,14 @@ class PosthogFlutterPlugin :
305306

306307
sdkName = "posthog-flutter"
307308
sdkVersion = postHogVersion
309+
310+
onFeatureFlags =
311+
PostHogOnFeatureFlags {
312+
Log.i("PostHogFlutter", "Android onFeatureFlags triggered. Notifying Dart.")
313+
invokeFlutterMethod("onFeatureFlagsCallback", emptyMap<String, Any?>())
314+
}
308315
}
316+
309317
PostHogAndroid.setup(applicationContext, config)
310318
}
311319

example/lib/main.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Future<void> main() async {
99
WidgetsFlutterBinding.ensureInitialized();
1010
final config =
1111
PostHogConfig('phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D');
12+
config.onFeatureFlags = () {
13+
debugPrint('[PostHog] Feature flags loaded!');
14+
};
1215
config.debug = true;
1316
config.captureApplicationLifecycleEvents = false;
1417
config.host = 'https://us.i.posthog.com';

ios/Classes/PosthogFlutterPlugin.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,47 @@ import PostHog
99

1010
public class PosthogFlutterPlugin: NSObject, FlutterPlugin {
1111
private static var instance: PosthogFlutterPlugin?
12+
private var channel: FlutterMethodChannel?
1213

1314
public static func getInstance() -> PosthogFlutterPlugin? {
1415
instance
1516
}
1617

18+
override init() {
19+
super.init()
20+
NotificationCenter.default.addObserver(
21+
self,
22+
selector: #selector(featureFlagsDidUpdate),
23+
name: PostHogSDK.didReceiveFeatureFlags,
24+
object: nil
25+
)
26+
}
27+
28+
deinit {
29+
NotificationCenter.default.removeObserver(self)
30+
}
31+
1732
public static func register(with registrar: FlutterPluginRegistrar) {
33+
let methodChannel: FlutterMethodChannel
1834
#if os(iOS)
19-
let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger())
35+
methodChannel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger())
2036
#elseif os(macOS)
21-
let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger)
37+
methodChannel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger)
2238
#endif
2339
let instance = PosthogFlutterPlugin()
24-
instance.channel = channel
40+
instance.channel = methodChannel
2541
PosthogFlutterPlugin.instance = instance
2642
initPlugin()
27-
registrar.addMethodCallDelegate(instance, channel: channel)
43+
registrar.addMethodCallDelegate(instance, channel: methodChannel)
44+
}
45+
46+
@objc func featureFlagsDidUpdate() {
47+
invokeFlutterMethod("onFeatureFlagsCallback", arguments: [String: Any]())
2848
}
2949

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

33-
private var channel: FlutterMethodChannel?
34-
3553
public static func initPlugin() {
3654
let autoInit = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.AUTO_INIT") as? Bool ?? true
3755
if !autoInit {

lib/posthog_flutter_web.dart

Lines changed: 205 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// In order to *not* need this ignore, consider extracting the "web" version
22
// of your plugin as a separate package, instead of inlining it in the same
33
// package as the core of your plugin.
4+
import 'dart:js_interop';
5+
46
import 'package:flutter/services.dart';
57
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
68

9+
import 'src/posthog_config.dart';
710
import 'src/posthog_flutter_platform_interface.dart';
811
import 'src/posthog_flutter_web_handler.dart';
912

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

1518
static void registerWith(Registrar registrar) {
16-
final MethodChannel channel = MethodChannel(
19+
final channel = MethodChannel(
1720
'posthog_flutter',
1821
const StandardMethodCodec(),
1922
registrar,
2023
);
21-
final PosthogFlutterWeb instance = PosthogFlutterWeb();
24+
final instance = PosthogFlutterWeb();
2225
channel.setMethodCallHandler(instance.handleMethodCall);
26+
27+
// Set the platform instance so that Posthog() can call methods directly
28+
// on the web implementation instead of going through the method channel.
29+
// This is required because method channels can only pass serializable data,
30+
// not function callbacks like onFeatureFlags.
31+
PosthogFlutterPlatformInterface.instance = instance;
32+
}
33+
34+
Future<dynamic> handleMethodCall(MethodCall call) async {
35+
// The 'setup' call is now handled by the setup method override.
36+
// Other method calls are delegated to handleWebMethodCall.
37+
if (call.method == 'setup') {
38+
// This case should ideally not be hit if Posthog().setup directly calls the overridden setup.
39+
// However, to be safe, we can log or ignore.
40+
// For now, let's assume direct call to overridden setup handles it.
41+
return null;
42+
}
43+
return handleWebMethodCall(call);
44+
}
45+
46+
@override
47+
Future<void> setup(PostHogConfig config) async {
48+
// It's assumed posthog-js is initialized by the user in their HTML.
49+
// This setup primarily hooks into the existing posthog-js instance.
50+
51+
// If apiKey and host are in config, and posthog.init is to be handled by plugin:
52+
// This is an example if we wanted the plugin to also call posthog.init()
53+
// final jsOptions = <String, dynamic>{
54+
// 'api_host': config.host,
55+
// // Add other relevant options from PostHogConfig if needed for JS init
56+
// }.jsify();
57+
// posthog?.callMethod('init'.toJS, config.apiKey.toJS, jsOptions);
58+
59+
final ph = posthog;
60+
if (config.onFeatureFlags != null && ph != null) {
61+
final dartCallback = config.onFeatureFlags!;
62+
63+
// JS SDK calls with: (flags: string[], variants: Record, context?: {errorsLoading})
64+
// We ignore the JS parameters and just invoke the void callback
65+
final jsCallback =
66+
(JSArray jsFlags, JSObject jsFlagVariants, [JSObject? jsContext]) {
67+
dartCallback();
68+
}.toJS;
69+
70+
ph.onFeatureFlags(jsCallback);
71+
}
72+
}
73+
74+
@override
75+
Future<void> identify({
76+
required String userId,
77+
Map<String, Object>? userProperties,
78+
Map<String, Object>? userPropertiesSetOnce,
79+
}) async {
80+
return handleWebMethodCall(MethodCall('identify', {
81+
'userId': userId,
82+
if (userProperties != null) 'userProperties': userProperties,
83+
if (userPropertiesSetOnce != null)
84+
'userPropertiesSetOnce': userPropertiesSetOnce,
85+
}));
86+
}
87+
88+
@override
89+
Future<void> capture({
90+
required String eventName,
91+
Map<String, Object>? properties,
92+
}) async {
93+
return handleWebMethodCall(MethodCall('capture', {
94+
'eventName': eventName,
95+
if (properties != null) 'properties': properties,
96+
}));
97+
}
98+
99+
@override
100+
Future<void> screen({
101+
required String screenName,
102+
Map<String, Object>? properties,
103+
}) async {
104+
return handleWebMethodCall(MethodCall('screen', {
105+
'screenName': screenName,
106+
if (properties != null) 'properties': properties,
107+
}));
108+
}
109+
110+
@override
111+
Future<void> alias({required String alias}) async {
112+
return handleWebMethodCall(MethodCall('alias', {'alias': alias}));
113+
}
114+
115+
@override
116+
Future<String> getDistinctId() async {
117+
final result = await handleWebMethodCall(const MethodCall('distinctId'));
118+
return result as String? ?? '';
119+
}
120+
121+
@override
122+
Future<void> reset() async {
123+
return handleWebMethodCall(const MethodCall('reset'));
124+
}
125+
126+
@override
127+
Future<void> disable() async {
128+
return handleWebMethodCall(const MethodCall('disable'));
129+
}
130+
131+
@override
132+
Future<void> enable() async {
133+
return handleWebMethodCall(const MethodCall('enable'));
134+
}
135+
136+
@override
137+
Future<bool> isOptOut() async {
138+
final result = await handleWebMethodCall(const MethodCall('isOptOut'));
139+
return result as bool? ?? true;
23140
}
24141

25-
Future<dynamic> handleMethodCall(MethodCall call) =>
26-
handleWebMethodCall(call);
142+
@override
143+
Future<void> debug(bool enabled) async {
144+
return handleWebMethodCall(MethodCall('debug', {'debug': enabled}));
145+
}
146+
147+
@override
148+
Future<void> register(String key, Object value) async {
149+
return handleWebMethodCall(
150+
MethodCall('register', {'key': key, 'value': value}));
151+
}
152+
153+
@override
154+
Future<void> unregister(String key) async {
155+
return handleWebMethodCall(MethodCall('unregister', {'key': key}));
156+
}
157+
158+
@override
159+
Future<bool> isFeatureEnabled(String key) async {
160+
final result =
161+
await handleWebMethodCall(MethodCall('isFeatureEnabled', {'key': key}));
162+
return result as bool? ?? false;
163+
}
164+
165+
@override
166+
Future<void> reloadFeatureFlags() async {
167+
return handleWebMethodCall(const MethodCall('reloadFeatureFlags'));
168+
}
169+
170+
@override
171+
Future<void> group({
172+
required String groupType,
173+
required String groupKey,
174+
Map<String, Object>? groupProperties,
175+
}) async {
176+
return handleWebMethodCall(MethodCall('group', {
177+
'groupType': groupType,
178+
'groupKey': groupKey,
179+
if (groupProperties != null) 'groupProperties': groupProperties,
180+
}));
181+
}
182+
183+
@override
184+
Future<Object?> getFeatureFlag({required String key}) async {
185+
return handleWebMethodCall(MethodCall('getFeatureFlag', {'key': key}));
186+
}
187+
188+
@override
189+
Future<Object?> getFeatureFlagPayload({required String key}) async {
190+
return handleWebMethodCall(
191+
MethodCall('getFeatureFlagPayload', {'key': key}));
192+
}
193+
194+
@override
195+
Future<void> flush() async {
196+
return handleWebMethodCall(const MethodCall('flush'));
197+
}
198+
199+
@override
200+
Future<void> close() async {
201+
return handleWebMethodCall(const MethodCall('close'));
202+
}
203+
204+
@override
205+
Future<String?> getSessionId() async {
206+
final result = await handleWebMethodCall(const MethodCall('getSessionId'));
207+
return result as String?;
208+
}
209+
210+
@override
211+
Future<void> openUrl(String url) async {
212+
// Not supported on web
213+
}
214+
215+
@override
216+
Future<void> showSurvey(Map<String, dynamic> survey) async {
217+
// Not supported on web - surveys handled by posthog-js
218+
}
219+
220+
@override
221+
Future<void> captureException({
222+
required Object error,
223+
StackTrace? stackTrace,
224+
Map<String, Object>? properties,
225+
}) async {
226+
// Not implemented on web
227+
}
27228
}

lib/src/error_tracking/isolate_handler_web.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// ignore_for_file: avoid_annotating_with_dynamic
2+
13
/// Web platform stub implementation of isolate error handling
24
/// Isolates are not available on web, so this is a no-op implementation
35
class IsolateErrorHandler {

lib/src/posthog.dart

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,25 @@ class Posthog {
1919

2020
String? _currentScreen;
2121

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

lib/src/posthog_config.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'posthog_flutter_platform_interface.dart';
2+
13
enum PostHogPersonProfiles { never, always, identifiedOnly }
24

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

47-
// TODO: missing getAnonymousId, propertiesSanitizer, captureDeepLinks
48-
// onFeatureFlags, integrations
49+
/// Callback to be invoked when feature flags are loaded.
50+
///
51+
/// Use [Posthog.getFeatureFlag] or [Posthog.isFeatureEnabled] within this
52+
/// callback to access the loaded flag values.
53+
OnFeatureFlagsCallback? onFeatureFlags;
54+
55+
// TODO: missing getAnonymousId, propertiesSanitizer, captureDeepLinks integrations
4956

50-
PostHogConfig(this.apiKey);
57+
PostHogConfig(
58+
this.apiKey, {
59+
this.onFeatureFlags,
60+
});
5161

5262
Map<String, dynamic> toMap() {
5363
return {

0 commit comments

Comments
 (0)