From 56f9120061a4f0848eae83ee201b13f90a3d9592 Mon Sep 17 00:00:00 2001 From: Ben Smith <5904336+bs1180@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:24:48 +0200 Subject: [PATCH 1/2] feat(surveys): add displaySurvey to show a survey on demand Adds Posthog().displaySurvey(surveyId), the Flutter counterpart of the web SDK's posthog.displaySurvey(). On Android and iOS the call routes through the new native displaySurvey APIs, which bypass display conditions and render through the existing Flutter survey UI; on Web it calls the JS SDK's displaySurvey directly. Bumps the native SDK minimums to the versions that will ship the API (posthog-android 3.56.0, posthog-ios 3.67.0). Generated-By: PostHog Code Task-Id: 96def33a-2460-42b7-9690-0a75ae8383fe --- .changeset/display-survey-manual-api.md | 5 ++ api/posthog_flutter.api.json | 66 +++++++++++++++++++ posthog_flutter/android/build.gradle | 2 +- .../posthog/flutter/PosthogFlutterPlugin.kt | 17 +++++ .../darwin/posthog_flutter.podspec | 4 +- .../darwin/posthog_flutter/Package.swift | 2 +- .../PosthogFlutterPlugin.swift | 19 ++++++ posthog_flutter/lib/src/posthog.dart | 18 +++++ .../lib/src/posthog_flutter_io.dart | 15 +++++ .../posthog_flutter_platform_interface.dart | 4 ++ .../lib/src/posthog_flutter_web_handler.dart | 13 ++++ .../test/posthog_flutter_io_test.dart | 8 +++ ...sthog_flutter_platform_interface_fake.dart | 8 +++ posthog_flutter/test/posthog_test.dart | 16 +++++ 14 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 .changeset/display-survey-manual-api.md diff --git a/.changeset/display-survey-manual-api.md b/.changeset/display-survey-manual-api.md new file mode 100644 index 00000000..3958c2ef --- /dev/null +++ b/.changeset/display-survey-manual-api.md @@ -0,0 +1,5 @@ +--- +"posthog_flutter": minor +--- + +Add `Posthog().displaySurvey(surveyId)` to display a survey on demand, bypassing display conditions (targeting flags, event triggers, and seen/wait-period checks). This is the counterpart of the web SDK's `posthog.displaySurvey()` and also enables API-type surveys, which are never auto-displayed. On Android and iOS the survey is displayed by the native SDK (requires posthog-android >= 3.56.0 / posthog-ios >= 3.67.0); on Web it is displayed by the JS SDK as a popover. diff --git a/api/posthog_flutter.api.json b/api/posthog_flutter.api.json index 1156b0f6..162a4a8b 100644 --- a/api/posthog_flutter.api.json +++ b/api/posthog_flutter.api.json @@ -713,6 +713,28 @@ "type": "method", "typeParameterNames": [] }, + { + "entryPoints": [], + "isDeprecated": false, + "isExperimental": false, + "isStatic": false, + "name": "displaySurvey", + "parameters": [ + { + "isDeprecated": false, + "isExperimental": false, + "isNamed": false, + "isRequired": true, + "name": "surveyId", + "relativePath": "lib/src/posthog_flutter_platform_interface.dart", + "typeName": "String" + } + ], + "relativePath": "lib/src/posthog_flutter_platform_interface.dart", + "returnTypeName": "Future", + "type": "method", + "typeParameterNames": [] + }, { "entryPoints": [], "isDeprecated": false, @@ -1595,6 +1617,28 @@ "type": "method", "typeParameterNames": [] }, + { + "entryPoints": [], + "isDeprecated": false, + "isExperimental": false, + "isStatic": false, + "name": "displaySurvey", + "parameters": [ + { + "isDeprecated": false, + "isExperimental": false, + "isNamed": false, + "isRequired": true, + "name": "surveyId", + "relativePath": "lib/src/posthog_flutter_platform_interface.dart", + "typeName": "String" + } + ], + "relativePath": "lib/src/posthog_flutter_platform_interface.dart", + "returnTypeName": "Future", + "type": "method", + "typeParameterNames": [] + }, { "entryPoints": [], "isDeprecated": false, @@ -4369,6 +4413,28 @@ "type": "method", "typeParameterNames": [] }, + { + "entryPoints": [], + "isDeprecated": false, + "isExperimental": false, + "isStatic": false, + "name": "displaySurvey", + "parameters": [ + { + "isDeprecated": false, + "isExperimental": false, + "isNamed": false, + "isRequired": true, + "name": "surveyId", + "relativePath": "lib/src/posthog.dart", + "typeName": "String" + } + ], + "relativePath": "lib/src/posthog.dart", + "returnTypeName": "Future", + "type": "method", + "typeParameterNames": [] + }, { "entryPoints": [], "isDeprecated": false, diff --git a/posthog_flutter/android/build.gradle b/posthog_flutter/android/build.gradle index 812eaf11..5d994c50 100644 --- a/posthog_flutter/android/build.gradle +++ b/posthog_flutter/android/build.gradle @@ -64,7 +64,7 @@ android { dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.0.0' - implementation 'com.posthog:posthog-android:[3.55.0,4.0.0)' + implementation 'com.posthog:posthog-android:[3.56.0,4.0.0)' } testOptions { diff --git a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt index 3cac8811..fe6d824f 100644 --- a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt +++ b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt @@ -392,6 +392,10 @@ class PosthogFlutterPlugin : handleSurveyAction(call, result) } + "displaySurvey" -> { + displaySurvey(call, result) + } + else -> { result.notImplemented() } @@ -1813,6 +1817,19 @@ class PosthogFlutterPlugin : } } + private fun displaySurvey( + call: MethodCall, + result: Result, + ) { + try { + val surveyId: String = call.argument("surveyId")!! + PostHog.displaySurvey(surveyId) + result.success(null) + } catch (e: Throwable) { + result.error("PosthogFlutterException", e.localizedMessage, null) + } + } + // MARK: - Survey Action Handling private fun handleSurveyAction( diff --git a/posthog_flutter/darwin/posthog_flutter.podspec b/posthog_flutter/darwin/posthog_flutter.podspec index 4458d19d..732123ab 100644 --- a/posthog_flutter/darwin/posthog_flutter.podspec +++ b/posthog_flutter/darwin/posthog_flutter.podspec @@ -21,8 +21,8 @@ Postog flutter plugin s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' - # ~> Version 3.66.0 up to, but not including, 4.0.0 - s.dependency 'PostHog', '>= 3.66.0', '< 4.0.0' + # ~> Version 3.67.0 up to, but not including, 4.0.0 + s.dependency 'PostHog', '>= 3.67.0', '< 4.0.0' s.ios.deployment_target = '13.0' # PH iOS SDK 3.0.0 requires >= 10.15 diff --git a/posthog_flutter/darwin/posthog_flutter/Package.swift b/posthog_flutter/darwin/posthog_flutter/Package.swift index 3998a97d..e3347a1b 100644 --- a/posthog_flutter/darwin/posthog_flutter/Package.swift +++ b/posthog_flutter/darwin/posthog_flutter/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(name: "FlutterFramework", path: "../FlutterFramework"), - .package(url: "https://github.com/PostHog/posthog-ios", "3.66.0" ..< "4.0.0"), + .package(url: "https://github.com/PostHog/posthog-ios", "3.67.0" ..< "4.0.0"), ], targets: [ .target( diff --git a/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift b/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift index 61adf6f4..b5b38d53 100644 --- a/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift +++ b/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift @@ -422,6 +422,13 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { // surveys only supported on iOS result(nil) #endif + case "displaySurvey": + #if os(iOS) + displaySurvey(call, result: result) + #else + // surveys only supported on iOS + result(nil) + #endif default: result(FlutterMethodNotImplemented) } @@ -461,6 +468,18 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { invokeFlutterMethod("hideSurveys", arguments: nil) } + private func displaySurvey(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let surveyId = args["surveyId"] as? String + else { + result(FlutterError(code: "InvalidArguments", message: "Missing surveyId", details: nil)) + return + } + + PostHogSDK.shared.displaySurvey(surveyId) + result(nil) + } + private func handleSurveyAction(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let survey = currentSurvey, let args = call.arguments as? [String: Any], diff --git a/posthog_flutter/lib/src/posthog.dart b/posthog_flutter/lib/src/posthog.dart index b38368e9..3258608b 100644 --- a/posthog_flutter/lib/src/posthog.dart +++ b/posthog_flutter/lib/src/posthog.dart @@ -765,5 +765,23 @@ class Posthog { /// current platform. Future isSessionReplayActive() => _posthog.isSessionReplayActive(); + /// Displays the survey with the given ID on demand, regardless of its + /// display conditions. + /// + /// The survey must be running and part of your project's surveys. Display + /// conditions (targeting flags, event triggers, and the seen/wait-period + /// checks) are bypassed, so this also works for API-type surveys, which are + /// never displayed automatically. If another survey is already being + /// displayed, this call is ignored. + /// + /// Surveys must be enabled via `PostHogConfig.surveys` (mobile) or in your + /// web snippet configuration. On Android and iOS the survey is rendered by + /// the native SDK through the regular Flutter survey UI; on Web it is + /// rendered by the JS SDK as a popover. + /// + /// Returns a [Future] that completes when the display request has been sent. + Future displaySurvey(String surveyId) => + _posthog.displaySurvey(surveyId); + Posthog._internal(); } diff --git a/posthog_flutter/lib/src/posthog_flutter_io.dart b/posthog_flutter/lib/src/posthog_flutter_io.dart index e38ad308..405458cf 100644 --- a/posthog_flutter/lib/src/posthog_flutter_io.dart +++ b/posthog_flutter/lib/src/posthog_flutter_io.dart @@ -511,6 +511,21 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { } } + @override + Future displaySurvey(String surveyId) async { + if (!isSupportedPlatform()) { + return; + } + + try { + await _methodChannel.invokeMethod('displaySurvey', { + 'surveyId': surveyId, + }); + } on PlatformException catch (exception) { + printIfDebug('Exception on displaySurvey: $exception'); + } + } + @override Future isFeatureEnabled(String key) async { if (!isSupportedPlatform()) { diff --git a/posthog_flutter/lib/src/posthog_flutter_platform_interface.dart b/posthog_flutter/lib/src/posthog_flutter_platform_interface.dart index 5eda98aa..454681d3 100644 --- a/posthog_flutter/lib/src/posthog_flutter_platform_interface.dart +++ b/posthog_flutter/lib/src/posthog_flutter_platform_interface.dart @@ -165,6 +165,10 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface { throw UnimplementedError('showSurvey() has not been implemented.'); } + Future displaySurvey(String surveyId) { + throw UnimplementedError('displaySurvey() has not been implemented.'); + } + Future group({ required String groupType, required String groupKey, diff --git a/posthog_flutter/lib/src/posthog_flutter_web_handler.dart b/posthog_flutter/lib/src/posthog_flutter_web_handler.dart index 41db48d9..2e6779c7 100644 --- a/posthog_flutter/lib/src/posthog_flutter_web_handler.dart +++ b/posthog_flutter/lib/src/posthog_flutter_web_handler.dart @@ -66,6 +66,8 @@ extension PostHogExtension on PostHog { external void startSessionRecording(); external void stopSessionRecording(); external bool sessionRecordingStarted(); + // May be absent on older posthog-js builds; the call site guards with try/catch. + external void displaySurvey(JSAny surveyId, JSAny options); external SessionManager? get sessionManager; // ignore: non_constant_identifier_names external void _overrideSDKInfo(JSAny sdkName, JSAny sdkVersion); @@ -420,6 +422,17 @@ Future handleWebMethodCall(MethodCall call) async { case 'surveyAction': // not supported on Web break; + case 'displaySurvey': + final surveyId = args['surveyId'] as String; + try { + posthog?.displaySurvey( + stringToJSAny(surveyId), + mapToJSAny({'displayType': 'popover'}), + ); + } catch (e) { + printIfDebug('Exception on displaySurvey: $e'); + } + break; case 'captureException': final properties = safeMapConversion(args['properties']); properties.addAll(_getLocationProperties()); diff --git a/posthog_flutter/test/posthog_flutter_io_test.dart b/posthog_flutter/test/posthog_flutter_io_test.dart index 844bffc1..c529dcf5 100644 --- a/posthog_flutter/test/posthog_flutter_io_test.dart +++ b/posthog_flutter/test/posthog_flutter_io_test.dart @@ -283,6 +283,14 @@ void main() { ]); }); + test('displaySurvey sends the surveyId over the method channel', () async { + await posthogFlutterIO.displaySurvey('survey-123'); + + final call = log.firstWhere((c) => c.method == 'displaySurvey'); + final args = Map.from(call.arguments as Map); + expect(args, {'surveyId': 'survey-123'}); + }); + test('setGroupPropertiesForFlags sends groupType and properties', () async { await posthogFlutterIO.setGroupPropertiesForFlags( 'organization', diff --git a/posthog_flutter/test/posthog_flutter_platform_interface_fake.dart b/posthog_flutter/test/posthog_flutter_platform_interface_fake.dart index 9a41fcd2..e665d7a1 100644 --- a/posthog_flutter/test/posthog_flutter_platform_interface_fake.dart +++ b/posthog_flutter/test/posthog_flutter_platform_interface_fake.dart @@ -187,6 +187,14 @@ class PosthogFlutterPlatformFake extends PosthogFlutterPlatformInterface { }); } + // Call tracking for displaySurvey + final List displaySurveyCalls = []; + + @override + Future displaySurvey(String surveyId) async { + displaySurveyCalls.add(surveyId); + } + @override Future getFeatureFlag({required String key}) async { return featureFlagValues[key]; diff --git a/posthog_flutter/test/posthog_test.dart b/posthog_flutter/test/posthog_test.dart index d4ffa76c..ecb5a8dc 100644 --- a/posthog_flutter/test/posthog_test.dart +++ b/posthog_flutter/test/posthog_test.dart @@ -680,6 +680,22 @@ void main() { }); }); + group('Posthog displaySurvey', () { + late PosthogFlutterPlatformFake fake; + + setUp(() async { + fake = PosthogFlutterPlatformFake(); + PosthogFlutterPlatformInterface.instance = fake; + await Posthog().close(); + }); + + test('forwards the survey ID to the platform interface', () async { + await Posthog().displaySurvey('survey-123'); + + expect(fake.displaySurveyCalls, ['survey-123']); + }); + }); + group('PostHogExceptionStepsConfig', () { test('toMap serializes the native defaults', () { final config = PostHogConfig('test_project_token'); From b7e3f1a0baa761952cb9fbc2c07d72fa1fa0c736 Mon Sep 17 00:00:00 2001 From: Ben Smith <5904336+bs1180@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:27:10 +0200 Subject: [PATCH 2/2] fix(surveys): wire displaySurvey through the web platform implementation PosthogFlutterWeb overrides each platform-interface method explicitly, so without a displaySurvey override the call fell through to the base class UnimplementedError and the web handler case was unreachable. Adds the override plus a browser test guarding the wiring, following the existing addExceptionStep pattern. Generated-By: PostHog Code Task-Id: 96def33a-2460-42b7-9690-0a75ae8383fe --- posthog_flutter/lib/posthog_flutter_web.dart | 7 +++++ .../posthog_flutter_web_handler_test.dart | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/posthog_flutter/lib/posthog_flutter_web.dart b/posthog_flutter/lib/posthog_flutter_web.dart index e62f72b1..b5024422 100644 --- a/posthog_flutter/lib/posthog_flutter_web.dart +++ b/posthog_flutter/lib/posthog_flutter_web.dart @@ -364,6 +364,13 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface { // Not supported on web - surveys handled by posthog-js } + @override + Future displaySurvey(String surveyId) async { + return handleWebMethodCall( + MethodCall('displaySurvey', {'surveyId': surveyId}), + ); + } + @override Future captureException({ required Object error, diff --git a/posthog_flutter/test/posthog_flutter_web_handler_test.dart b/posthog_flutter/test/posthog_flutter_web_handler_test.dart index 66975f1d..5c455fcb 100644 --- a/posthog_flutter/test/posthog_flutter_web_handler_test.dart +++ b/posthog_flutter/test/posthog_flutter_web_handler_test.dart @@ -156,6 +156,37 @@ void main() { }); }); + // Guards the web wiring: PosthogFlutterWeb must override displaySurvey so + // the call actually reaches posthog-js. Without the override it falls through + // to the platform interface and throws UnimplementedError on web. + group('PosthogFlutterWeb displaySurvey', () { + String? capturedSurveyId; + JSAny? capturedOptions; + + setUp(() { + capturedSurveyId = null; + capturedOptions = null; + + final fake = JSObject(); + fake.setProperty( + 'displaySurvey'.toJS, + ((JSString surveyId, JSAny? options) { + capturedSurveyId = surveyId.toDart; + capturedOptions = options; + }).toJS, + ); + globalContext.setProperty('posthog'.toJS, fake); + }); + + test('override forwards the survey ID and popover options to posthog-js', + () async { + await PosthogFlutterWeb().displaySurvey('survey-123'); + + expect(capturedSurveyId, 'survey-123'); + expect(capturedOptions.dartify(), {'displayType': 'popover'}); + }); + }); + // captureException must route through posthog-js's captureException (not the // generic capture) so it attaches required metadata and any buffered // $exception_steps. The Dart-built payload is passed as additionalProperties,