Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/survey-popup-delay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog_flutter": minor
---

Wait `surveyPopupDelaySeconds` before showing a survey. A missing, zero, or negative delay still shows it immediately.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ fun PostHogDisplaySurvey.toMap(): Map<String, Any?> {
"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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class PostHogDisplaySurveyAppearance {
this.inputBackground,
this.inputTextColor,
this.placeholder,
this.surveyPopupDelaySeconds,
this.displayThankYouMessage = true,
this.thankYouMessageHeader,
this.thankYouMessageDescription,
Expand All @@ -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;
Expand Down
84 changes: 72 additions & 12 deletions posthog_flutter/lib/src/surveys/survey_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ 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.
/// Values above one day are capped at one day.
Duration surveyPopupDelay(double? seconds) {
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());
Comment thread
o-mid marked this conversation as resolved.
}

/// A service that manages displaying surveys
///
/// This service uses the PosthogObserver to access the current navigation context
Expand All @@ -26,7 +40,9 @@ class SurveyService {
Route<dynamic>? _currentSurveyRoute;
PostHogDisplaySurvey? _currentSurvey;
Completer<void>? _programmaticDismissal;
Completer<bool>? _contextWait;
Completer<bool>? _pendingShow;
Timer? _popupDelayTimer;
PostHogDisplaySurvey? _delayedSurvey;

/// Shows a survey using the PosthogObserver context
Future<void> showSurvey(
Expand All @@ -40,6 +56,13 @@ class SurveyService {
return;
}

_cancelPopupDelay();
Comment thread
o-mid marked this conversation as resolved.

final delay = surveyPopupDelay(survey.appearance?.surveyPopupDelaySeconds);
if (delay > Duration.zero && !await _waitForPopupDelay(survey, delay)) {
return;
}

var context = PosthogObserver.currentContext;
if (context == null) {
printIfDebug(
Expand All @@ -64,24 +87,43 @@ 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<bool> _waitForPopupDelay(
PostHogDisplaySurvey survey,
Duration delay,
) {
final wait = _startPendingShow();
_delayedSurvey = survey;
_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<bool> _waitForContext() {
_endContextWait(show: false);
final contextWait = Completer<bool>();
_contextWait = contextWait;
return contextWait.future;
Future<bool> _waitForContext() => _startPendingShow();

Future<bool> _startPendingShow() {
_endPendingShow(show: false);
final pendingShow = Completer<bool>();
_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<void> _showSurveyWithNavigator(
Expand Down Expand Up @@ -174,9 +216,27 @@ 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() {
if (_delayedSurvey == null) return;

_popupDelayTimer?.cancel();
_clearPopupDelay();
_endPendingShow(show: false);
}

void _clearPopupDelay() {
_popupDelayTimer = null;
_delayedSurvey = null;
}

/// Hides any active survey
void hideSurvey({PostHogDisplaySurvey? survey}) {
if (survey == null) _endContextWait(show: false);
if (survey == null || identical(survey, _delayedSurvey)) {
_cancelPopupDelay();
}
if (survey == null) _endPendingShow(show: false);
if (survey != null && !identical(survey, _currentSurvey)) return;
if (!_isShowingSurvey || _isDismissingSurvey) {
return;
Expand Down
Loading
Loading