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/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/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_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, 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');