From fc82b6a6cd705c59cdbe961a7169f2aee6604225 Mon Sep 17 00:00:00 2001 From: Omid Mirzaei Date: Wed, 30 Sep 2026 17:32:13 +0400 Subject: [PATCH 1/3] feat(surveys): honor surveyPopupDelaySeconds before showing Wire the appearance field through native maps and Dart models, wait in SurveyService with cancel/replace semantics, and add widget tests. Fixes #322 --- .changeset/survey-popup-delay.md | 5 + .../flutter/PostHogDisplaySurveyExt.kt | 1 + .../PostHogDisplaySurvey+Dict.swift | 3 + .../models/posthog_display_survey.dart | 3 + .../posthog_display_survey_appearance.dart | 6 + .../lib/src/surveys/survey_service.dart | 59 ++++++ .../test/survey_popup_delay_test.dart | 168 ++++++++++++++++++ 7 files changed, 245 insertions(+) create mode 100644 .changeset/survey-popup-delay.md create mode 100644 posthog_flutter/test/survey_popup_delay_test.dart diff --git a/.changeset/survey-popup-delay.md b/.changeset/survey-popup-delay.md new file mode 100644 index 00000000..4042690d --- /dev/null +++ b/.changeset/survey-popup-delay.md @@ -0,0 +1,5 @@ +--- +"posthog_flutter": patch +--- + +Wait `surveyPopupDelaySeconds` before showing a survey. A missing, zero, or negative delay still shows it immediately. diff --git a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PostHogDisplaySurveyExt.kt b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PostHogDisplaySurveyExt.kt index e158e0bf..0d2b7a33 100644 --- a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PostHogDisplaySurveyExt.kt +++ b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PostHogDisplaySurveyExt.kt @@ -79,6 +79,7 @@ fun PostHogDisplaySurvey.toMap(): Map { "inputBackground" to app.inputBackground, "inputTextColor" to app.inputTextColor, "placeholder" to app.placeholder, + "surveyPopupDelaySeconds" to app.surveyPopupDelaySeconds, "displayThankYouMessage" to app.displayThankYouMessage, "thankYouMessageHeader" to app.thankYouMessageHeader, "thankYouMessageDescription" to app.thankYouMessageDescription, diff --git a/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PostHogDisplaySurvey+Dict.swift b/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PostHogDisplaySurvey+Dict.swift index a9bd99d6..f9005fcd 100644 --- a/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PostHogDisplaySurvey+Dict.swift +++ b/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PostHogDisplaySurvey+Dict.swift @@ -93,6 +93,9 @@ if let placeholder = appearance.placeholder { appearanceDict["placeholder"] = placeholder } + if let surveyPopupDelaySeconds = appearance.surveyPopupDelaySeconds { + appearanceDict["surveyPopupDelaySeconds"] = surveyPopupDelaySeconds + } appearanceDict["displayThankYouMessage"] = appearance.displayThankYouMessage if let thankYouMessageHeader = appearance.thankYouMessageHeader { appearanceDict["thankYouMessageHeader"] = thankYouMessageHeader diff --git a/posthog_flutter/lib/src/surveys/models/posthog_display_survey.dart b/posthog_flutter/lib/src/surveys/models/posthog_display_survey.dart index ce4affeb..4237154e 100644 --- a/posthog_flutter/lib/src/surveys/models/posthog_display_survey.dart +++ b/posthog_flutter/lib/src/surveys/models/posthog_display_survey.dart @@ -115,6 +115,9 @@ class PostHogDisplaySurvey { inputBackground: a['inputBackground'] as String?, inputTextColor: a['inputTextColor'] as String?, placeholder: a['placeholder'] as String?, + surveyPopupDelaySeconds: a['surveyPopupDelaySeconds'] is num + ? (a['surveyPopupDelaySeconds'] as num).toDouble() + : null, displayThankYouMessage: a['displayThankYouMessage'] as bool? ?? true, thankYouMessageHeader: a['thankYouMessageHeader'] as String?, thankYouMessageDescription: a['thankYouMessageDescription'] as String?, diff --git a/posthog_flutter/lib/src/surveys/models/posthog_display_survey_appearance.dart b/posthog_flutter/lib/src/surveys/models/posthog_display_survey_appearance.dart index 8cf2d565..32b98b51 100644 --- a/posthog_flutter/lib/src/surveys/models/posthog_display_survey_appearance.dart +++ b/posthog_flutter/lib/src/surveys/models/posthog_display_survey_appearance.dart @@ -18,6 +18,7 @@ class PostHogDisplaySurveyAppearance { this.inputBackground, this.inputTextColor, this.placeholder, + this.surveyPopupDelaySeconds, this.displayThankYouMessage = true, this.thankYouMessageHeader, this.thankYouMessageDescription, @@ -43,6 +44,11 @@ class PostHogDisplaySurveyAppearance { final String? inputBackground; final String? inputTextColor; final String? placeholder; + + /// Seconds to wait after the survey is triggered before it is shown. + /// + /// Null or a non-positive value shows the survey immediately. + final double? surveyPopupDelaySeconds; final bool displayThankYouMessage; final String? thankYouMessageHeader; final String? thankYouMessageDescription; diff --git a/posthog_flutter/lib/src/surveys/survey_service.dart b/posthog_flutter/lib/src/surveys/survey_service.dart index 2ea8b32a..63ad622a 100644 --- a/posthog_flutter/lib/src/surveys/survey_service.dart +++ b/posthog_flutter/lib/src/surveys/survey_service.dart @@ -9,6 +9,17 @@ import 'models/survey_callbacks.dart'; import 'models/survey_appearance.dart'; import 'widgets/survey_bottom_sheet.dart'; +/// How long to wait before showing a survey. +/// +/// [seconds] comes from the survey's `surveyPopupDelaySeconds`. Null, zero, +/// and negative values show the survey immediately, matching iOS and Android. +Duration surveyPopupDelay(double? seconds) { + if (seconds == null || seconds.isNaN || seconds.isInfinite || seconds <= 0) { + return Duration.zero; + } + return Duration(milliseconds: (seconds * 1000).round()); +} + /// A service that manages displaying surveys /// /// This service uses the PosthogObserver to access the current navigation context @@ -27,6 +38,11 @@ class SurveyService { PostHogDisplaySurvey? _currentSurvey; Completer? _programmaticDismissal; Completer? _contextWait; + Timer? _popupDelayTimer; + Completer? _popupDelayGate; + int _popupDelayGeneration = 0; + PostHogDisplaySurvey? _pendingSurvey; + OnSurveyClosed? _pendingOnClosed; /// Shows a survey using the PosthogObserver context Future showSurvey( @@ -40,6 +56,28 @@ class SurveyService { return; } + // A survey still waiting out its delay has not been shown. Replacing it + // closes that presentation the same way the iOS surveys delegate does. + _cancelPendingSurvey(); + + final delay = surveyPopupDelay(survey.appearance?.surveyPopupDelaySeconds); + if (delay > Duration.zero) { + final generation = ++_popupDelayGeneration; + final gate = Completer(); + _popupDelayGate = gate; + _pendingSurvey = survey; + _pendingOnClosed = onClosed; + _popupDelayTimer = Timer(delay, () { + if (!gate.isCompleted) gate.complete(); + }); + await gate.future; + if (generation != _popupDelayGeneration) return; + _popupDelayTimer = null; + _popupDelayGate = null; + _pendingSurvey = null; + _pendingOnClosed = null; + } + var context = PosthogObserver.currentContext; if (context == null) { printIfDebug( @@ -174,9 +212,30 @@ class SurveyService { route.navigator?.removeRoute(route); } + void _cancelPendingSurvey() { + final survey = _pendingSurvey; + final onClosed = _pendingOnClosed; + if (survey == null) return; + + _popupDelayGeneration++; + _popupDelayTimer?.cancel(); + _popupDelayTimer = null; + final gate = _popupDelayGate; + _popupDelayGate = null; + _pendingSurvey = null; + _pendingOnClosed = null; + if (gate != null && !gate.isCompleted) gate.complete(); + onClosed?.call(survey); + } + /// Hides any active survey void hideSurvey({PostHogDisplaySurvey? survey}) { if (survey == null) _endContextWait(show: false); + if (_pendingSurvey != null && + (survey == null || identical(survey, _pendingSurvey))) { + _cancelPendingSurvey(); + if (!_isShowingSurvey) return; + } if (survey != null && !identical(survey, _currentSurvey)) return; if (!_isShowingSurvey || _isDismissingSurvey) { return; diff --git a/posthog_flutter/test/survey_popup_delay_test.dart b/posthog_flutter/test/survey_popup_delay_test.dart new file mode 100644 index 00000000..b5bfdf5a --- /dev/null +++ b/posthog_flutter/test/survey_popup_delay_test.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:posthog_flutter/src/posthog_observer.dart'; +import 'package:posthog_flutter/src/surveys/models/posthog_display_survey.dart'; +import 'package:posthog_flutter/src/surveys/survey_service.dart'; +import 'package:posthog_flutter/src/surveys/widgets/survey_bottom_sheet.dart'; + +void main() { + Map survey({Object? delaySeconds}) { + return { + 'id': 'survey-1', + 'name': 'Test survey', + 'questions': [ + { + 'type': 'link', + 'question': 'Welcome', + 'isOptional': false, + 'link': '', + }, + ], + if (delaySeconds != null) + 'appearance': {'surveyPopupDelaySeconds': delaySeconds}, + }; + } + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Navigator( + observers: [PosthogObserver()], + onGenerateRoute: (_) => MaterialPageRoute( + builder: (_) => const Scaffold(body: Text('Home')), + ), + ), + ), + ); + await tester.pumpAndSettle(); + } + + tearDown(() async { + SurveyService().hideSurvey(); + PosthogObserver.clearCurrentContext(); + }); + + testWidgets( + 'shows a survey immediately when the popup delay is unset or not positive', + ( + tester, + ) async { + await pumpApp(tester); + + for (final delay in [null, 0, -1]) { + final shown = SurveyService().showSurvey( + PostHogDisplaySurvey.fromDict(survey(delaySeconds: delay)), + (_) {}, + (_, __, ___) async => null, + (_) {}, + ); + await tester.pump(); + + expect(find.byType(SurveyBottomSheet), findsOneWidget); + SurveyService().hideSurvey(); + await tester.pumpAndSettle(); + await shown; + } + }); + + testWidgets('waits surveyPopupDelaySeconds before showing the survey', ( + tester, + ) async { + await pumpApp(tester); + final closed = []; + + final shown = SurveyService().showSurvey( + PostHogDisplaySurvey.fromDict(survey(delaySeconds: 2)), + (_) {}, + (_, __, ___) async => null, + (survey) => closed.add(survey.id), + ); + await tester.pump(); + + expect(find.byType(SurveyBottomSheet), findsNothing); + expect(closed, isEmpty); + + await tester.pump(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + + expect(find.byType(SurveyBottomSheet), findsOneWidget); + expect(closed, isEmpty); + + SurveyService().hideSurvey(); + await tester.pumpAndSettle(); + await shown; + }); + + testWidgets('does not show a survey hidden during its popup delay', ( + tester, + ) async { + await pumpApp(tester); + final closed = []; + + final shown = SurveyService().showSurvey( + PostHogDisplaySurvey.fromDict(survey(delaySeconds: 5)), + (_) {}, + (_, __, ___) async => null, + (survey) => closed.add(survey.id), + ); + await tester.pump(); + + SurveyService().hideSurvey(); + await tester.pump(const Duration(seconds: 5)); + await tester.pumpAndSettle(); + await shown; + + expect(find.byType(SurveyBottomSheet), findsNothing); + expect(closed, ['survey-1']); + }); + + testWidgets('a later survey replaces one still waiting on its delay', ( + tester, + ) async { + await pumpApp(tester); + final closed = []; + + final first = SurveyService().showSurvey( + PostHogDisplaySurvey.fromDict({ + ...survey(delaySeconds: 10), + 'id': 'first', + }), + (_) {}, + (_, __, ___) async => null, + (survey) => closed.add(survey.id), + ); + await tester.pump(); + + final second = SurveyService().showSurvey( + PostHogDisplaySurvey.fromDict({ + ...survey(delaySeconds: 1), + 'id': 'second', + 'questions': [ + { + 'type': 'link', + 'question': 'Second', + 'isOptional': false, + 'link': '', + }, + ], + }), + (_) {}, + (_, __, ___) async => null, + (survey) => closed.add(survey.id), + ); + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + + expect(find.text('Second'), findsOneWidget); + expect(find.text('Welcome'), findsNothing); + expect(closed, ['first']); + + SurveyService().hideSurvey(); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 10)); + await first; + await second; + + expect(find.byType(SurveyBottomSheet), findsNothing); + }); +} From 4c8283db0f6889f37b2c48ec28862a8930b35f73 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 7 Oct 2026 12:01:35 -0400 Subject: [PATCH 2/3] fix(surveys): address review on popup delay Wait out the delay on the same pending-show completer the context wait uses, so a survey with no context after its delay waits for the next navigation instead of being closed, and hideSurvey or a newer survey cancels either wait. A navigation during the delay no longer cuts it short. Cap the delay at one day so huge values can't overflow, drop the redundant generation counter, and make the changeset a minor. Co-authored-by: Omid Mirzaei Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/survey-popup-delay.md | 2 +- .../lib/src/surveys/survey_service.dart | 103 ++++++++++-------- .../test/survey_popup_delay_test.dart | 75 ++++++++++++- 3 files changed, 131 insertions(+), 49 deletions(-) diff --git a/.changeset/survey-popup-delay.md b/.changeset/survey-popup-delay.md index 4042690d..5b2af3a7 100644 --- a/.changeset/survey-popup-delay.md +++ b/.changeset/survey-popup-delay.md @@ -1,5 +1,5 @@ --- -"posthog_flutter": patch +"posthog_flutter": minor --- Wait `surveyPopupDelaySeconds` before showing a survey. A missing, zero, or negative delay still shows it immediately. diff --git a/posthog_flutter/lib/src/surveys/survey_service.dart b/posthog_flutter/lib/src/surveys/survey_service.dart index 63ad622a..f19c3e9c 100644 --- a/posthog_flutter/lib/src/surveys/survey_service.dart +++ b/posthog_flutter/lib/src/surveys/survey_service.dart @@ -13,10 +13,13 @@ import 'widgets/survey_bottom_sheet.dart'; /// /// [seconds] comes from the survey's `surveyPopupDelaySeconds`. Null, zero, /// and negative values show the survey immediately, matching iOS and Android. +/// Values above one day are capped at one day. Duration surveyPopupDelay(double? seconds) { - if (seconds == null || seconds.isNaN || seconds.isInfinite || seconds <= 0) { + if (seconds == null || seconds.isNaN || seconds <= 0) { return Duration.zero; } + const maxDelay = Duration(days: 1); + if (seconds >= maxDelay.inSeconds) return maxDelay; return Duration(milliseconds: (seconds * 1000).round()); } @@ -37,12 +40,10 @@ class SurveyService { Route? _currentSurveyRoute; PostHogDisplaySurvey? _currentSurvey; Completer? _programmaticDismissal; - Completer? _contextWait; + Completer? _pendingShow; Timer? _popupDelayTimer; - Completer? _popupDelayGate; - int _popupDelayGeneration = 0; - PostHogDisplaySurvey? _pendingSurvey; - OnSurveyClosed? _pendingOnClosed; + PostHogDisplaySurvey? _delayedSurvey; + OnSurveyClosed? _delayedOnClosed; /// Shows a survey using the PosthogObserver context Future showSurvey( @@ -58,24 +59,12 @@ class SurveyService { // A survey still waiting out its delay has not been shown. Replacing it // closes that presentation the same way the iOS surveys delegate does. - _cancelPendingSurvey(); + _cancelPopupDelay(); final delay = surveyPopupDelay(survey.appearance?.surveyPopupDelaySeconds); - if (delay > Duration.zero) { - final generation = ++_popupDelayGeneration; - final gate = Completer(); - _popupDelayGate = gate; - _pendingSurvey = survey; - _pendingOnClosed = onClosed; - _popupDelayTimer = Timer(delay, () { - if (!gate.isCompleted) gate.complete(); - }); - await gate.future; - if (generation != _popupDelayGeneration) return; - _popupDelayTimer = null; - _popupDelayGate = null; - _pendingSurvey = null; - _pendingOnClosed = null; + if (delay > Duration.zero && + !await _waitForPopupDelay(survey, onClosed, delay)) { + return; } var context = PosthogObserver.currentContext; @@ -102,24 +91,45 @@ class SurveyService { ); } + /// Completes with true once the delay has elapsed, or false when the wait + /// was cancelled by [hideSurvey] or replaced by a newer survey. + Future _waitForPopupDelay( + PostHogDisplaySurvey survey, + OnSurveyClosed onClosed, + Duration delay, + ) { + final wait = _startPendingShow(); + _delayedSurvey = survey; + _delayedOnClosed = onClosed; + _popupDelayTimer = Timer(delay, () { + _clearPopupDelay(); + _endPendingShow(show: true); + }); + return wait; + } + /// Completes with true once [PosthogObserver] reports a context, or false /// when the wait was cancelled by [hideSurvey] or replaced by a newer survey. - Future _waitForContext() { - _endContextWait(show: false); - final contextWait = Completer(); - _contextWait = contextWait; - return contextWait.future; + Future _waitForContext() => _startPendingShow(); + + Future _startPendingShow() { + _endPendingShow(show: false); + final pendingShow = Completer(); + _pendingShow = pendingShow; + return pendingShow.future; } - void _endContextWait({required bool show}) { - final contextWait = _contextWait; - _contextWait = null; - contextWait?.complete(show); + void _endPendingShow({required bool show}) { + final pendingShow = _pendingShow; + _pendingShow = null; + pendingShow?.complete(show); } /// Called by [PosthogObserver] when it has a context, so a survey that /// arrived before then can be shown. - void onContextAvailable() => _endContextWait(show: true); + void onContextAvailable() { + if (_popupDelayTimer == null) _endPendingShow(show: true); + } /// Shows a survey using a navigator context Future _showSurveyWithNavigator( @@ -212,30 +222,29 @@ class SurveyService { route.navigator?.removeRoute(route); } - void _cancelPendingSurvey() { - final survey = _pendingSurvey; - final onClosed = _pendingOnClosed; + void _cancelPopupDelay() { + final survey = _delayedSurvey; + final onClosed = _delayedOnClosed; if (survey == null) return; - _popupDelayGeneration++; _popupDelayTimer?.cancel(); - _popupDelayTimer = null; - final gate = _popupDelayGate; - _popupDelayGate = null; - _pendingSurvey = null; - _pendingOnClosed = null; - if (gate != null && !gate.isCompleted) gate.complete(); + _clearPopupDelay(); + _endPendingShow(show: false); onClosed?.call(survey); } + void _clearPopupDelay() { + _popupDelayTimer = null; + _delayedSurvey = null; + _delayedOnClosed = null; + } + /// Hides any active survey void hideSurvey({PostHogDisplaySurvey? survey}) { - if (survey == null) _endContextWait(show: false); - if (_pendingSurvey != null && - (survey == null || identical(survey, _pendingSurvey))) { - _cancelPendingSurvey(); - if (!_isShowingSurvey) return; + if (survey == null || identical(survey, _delayedSurvey)) { + _cancelPopupDelay(); } + if (survey == null) _endPendingShow(show: false); if (survey != null && !identical(survey, _currentSurvey)) return; if (!_isShowingSurvey || _isDismissingSurvey) { return; diff --git a/posthog_flutter/test/survey_popup_delay_test.dart b/posthog_flutter/test/survey_popup_delay_test.dart index b5bfdf5a..791c48bc 100644 --- a/posthog_flutter/test/survey_popup_delay_test.dart +++ b/posthog_flutter/test/survey_popup_delay_test.dart @@ -37,6 +37,14 @@ void main() { await tester.pumpAndSettle(); } + void pushNextRoute(WidgetTester tester) { + tester.state(find.byType(Navigator).last).push( + MaterialPageRoute( + builder: (_) => const Scaffold(body: Text('Next')), + ), + ); + } + tearDown(() async { SurveyService().hideSurvey(); PosthogObserver.clearCurrentContext(); @@ -82,7 +90,10 @@ void main() { expect(find.byType(SurveyBottomSheet), findsNothing); expect(closed, isEmpty); - await tester.pump(const Duration(seconds: 2)); + await tester.pump(const Duration(milliseconds: 1900)); + expect(find.byType(SurveyBottomSheet), findsNothing); + + await tester.pump(const Duration(milliseconds: 100)); await tester.pumpAndSettle(); expect(find.byType(SurveyBottomSheet), findsOneWidget); @@ -93,6 +104,68 @@ void main() { await shown; }); + testWidgets( + 'waits for the next navigation when there is no context after the delay', + (tester) async { + await pumpApp(tester); + PosthogObserver.clearCurrentContext(); + final closed = []; + + final shown = SurveyService().showSurvey( + PostHogDisplaySurvey.fromDict(survey(delaySeconds: 1)), + (_) {}, + (_, __, ___) async => null, + (survey) => closed.add(survey.id), + ); + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + + expect(find.byType(SurveyBottomSheet), findsNothing); + expect(closed, isEmpty); + + pushNextRoute(tester); + await tester.pumpAndSettle(); + + expect(find.byType(SurveyBottomSheet), findsOneWidget); + expect(closed, isEmpty); + + SurveyService().hideSurvey(); + await tester.pumpAndSettle(); + await shown; + }); + + testWidgets('a navigation during the delay does not show the survey early', ( + tester, + ) async { + await pumpApp(tester); + PosthogObserver.clearCurrentContext(); + + final shown = SurveyService().showSurvey( + PostHogDisplaySurvey.fromDict(survey(delaySeconds: 2)), + (_) {}, + (_, __, ___) async => null, + (_) {}, + ); + await tester.pump(); + + pushNextRoute(tester); + await tester.pumpAndSettle(); + expect(find.byType(SurveyBottomSheet), findsNothing); + + await tester.pump(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + expect(find.byType(SurveyBottomSheet), findsOneWidget); + + SurveyService().hideSurvey(); + await tester.pumpAndSettle(); + await shown; + }); + + test('caps a very large popup delay at one day', () { + expect(surveyPopupDelay(1e13), const Duration(days: 1)); + expect(surveyPopupDelay(double.maxFinite), const Duration(days: 1)); + }); + testWidgets('does not show a survey hidden during its popup delay', ( tester, ) async { From 375ea5157c9a2de8bff526e30e1a7e455e22c36f Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 7 Oct 2026 14:07:05 -0400 Subject: [PATCH 3/3] fix(surveys): address review findings on popup delay Don't call onClosed for a survey hidden or replaced during its popup delay. The user never saw it, so it must not be marked seen or send `survey dismissed`, matching the wait-for-context path and the Android surveys host. Native has already dropped that presentation by the time Flutter cancels the delay, so nothing is left waiting for a close. Make the replacement test fail if the replaced survey's timer keeps running and shows the newer survey early. Co-authored-by: Omid Mirzaei Co-Authored-By: Claude Opus 5.5 (1M context) --- .../lib/src/surveys/survey_service.dart | 16 ++++------------ .../test/survey_popup_delay_test.dart | 13 ++++++++----- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/posthog_flutter/lib/src/surveys/survey_service.dart b/posthog_flutter/lib/src/surveys/survey_service.dart index f19c3e9c..fc2be385 100644 --- a/posthog_flutter/lib/src/surveys/survey_service.dart +++ b/posthog_flutter/lib/src/surveys/survey_service.dart @@ -43,7 +43,6 @@ class SurveyService { Completer? _pendingShow; Timer? _popupDelayTimer; PostHogDisplaySurvey? _delayedSurvey; - OnSurveyClosed? _delayedOnClosed; /// Shows a survey using the PosthogObserver context Future showSurvey( @@ -57,13 +56,10 @@ class SurveyService { return; } - // A survey still waiting out its delay has not been shown. Replacing it - // closes that presentation the same way the iOS surveys delegate does. _cancelPopupDelay(); final delay = surveyPopupDelay(survey.appearance?.surveyPopupDelaySeconds); - if (delay > Duration.zero && - !await _waitForPopupDelay(survey, onClosed, delay)) { + if (delay > Duration.zero && !await _waitForPopupDelay(survey, delay)) { return; } @@ -95,12 +91,10 @@ class SurveyService { /// was cancelled by [hideSurvey] or replaced by a newer survey. Future _waitForPopupDelay( PostHogDisplaySurvey survey, - OnSurveyClosed onClosed, Duration delay, ) { final wait = _startPendingShow(); _delayedSurvey = survey; - _delayedOnClosed = onClosed; _popupDelayTimer = Timer(delay, () { _clearPopupDelay(); _endPendingShow(show: true); @@ -222,21 +216,19 @@ class SurveyService { route.navigator?.removeRoute(route); } + /// Drops a survey still waiting out its delay without calling onClosed: + /// the user never saw it, so it must not be marked seen or dismissed. void _cancelPopupDelay() { - final survey = _delayedSurvey; - final onClosed = _delayedOnClosed; - if (survey == null) return; + if (_delayedSurvey == null) return; _popupDelayTimer?.cancel(); _clearPopupDelay(); _endPendingShow(show: false); - onClosed?.call(survey); } void _clearPopupDelay() { _popupDelayTimer = null; _delayedSurvey = null; - _delayedOnClosed = null; } /// Hides any active survey diff --git a/posthog_flutter/test/survey_popup_delay_test.dart b/posthog_flutter/test/survey_popup_delay_test.dart index 791c48bc..f762d4bb 100644 --- a/posthog_flutter/test/survey_popup_delay_test.dart +++ b/posthog_flutter/test/survey_popup_delay_test.dart @@ -186,7 +186,7 @@ void main() { await shown; expect(find.byType(SurveyBottomSheet), findsNothing); - expect(closed, ['survey-1']); + expect(closed, isEmpty); }); testWidgets('a later survey replaces one still waiting on its delay', ( @@ -197,7 +197,7 @@ void main() { final first = SurveyService().showSurvey( PostHogDisplaySurvey.fromDict({ - ...survey(delaySeconds: 10), + ...survey(delaySeconds: 1), 'id': 'first', }), (_) {}, @@ -208,7 +208,7 @@ void main() { final second = SurveyService().showSurvey( PostHogDisplaySurvey.fromDict({ - ...survey(delaySeconds: 1), + ...survey(delaySeconds: 2), 'id': 'second', 'questions': [ { @@ -223,16 +223,19 @@ void main() { (_, __, ___) async => null, (survey) => closed.add(survey.id), ); + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + expect(find.byType(SurveyBottomSheet), findsNothing); + await tester.pump(const Duration(seconds: 1)); await tester.pumpAndSettle(); expect(find.text('Second'), findsOneWidget); expect(find.text('Welcome'), findsNothing); - expect(closed, ['first']); + expect(closed, isEmpty); SurveyService().hideSurvey(); await tester.pumpAndSettle(); - await tester.pump(const Duration(seconds: 10)); await first; await second;