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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- chore: improve survey color handling ([#233](https://github.com/PostHog/posthog-flutter/pull/233))

- feat: add `beforeSend` callback to `PostHogConfig` for dropping or modifying events before they are sent to PostHog ([#255](https://github.com/PostHog/posthog-flutter/pull/255))
- **Limitation**:
- Does NOT intercept native-initiated events such as:
Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ android {
dependencies {
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.mockito:mockito-core:5.0.0'
// + Version 3.25.0 and the versions up to 4.0.0, not including 4.0.0 and higher
implementation 'com.posthog:posthog-android:[3.25.0,4.0.0]'
// + Version 3.30.0 and the versions up to 4.0.0, not including 4.0.0 and higher
implementation 'com.posthog:posthog-android:[3.30.0,4.0.0]'
}

testOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,24 @@ fun PostHogDisplaySurvey.toMap(): Map<String, Any?> {
appearance?.let { app ->
map["appearance"] =
mapOf(
"fontFamily" to app.fontFamily,
"backgroundColor" to app.backgroundColor,
"borderColor" to app.borderColor,
"submitButtonColor" to app.submitButtonColor,
"submitButtonText" to app.submitButtonText,
"submitButtonTextColor" to app.submitButtonTextColor,
"textColor" to app.textColor,
"descriptionTextColor" to app.descriptionTextColor,
"ratingButtonColor" to app.ratingButtonColor,
"ratingButtonActiveColor" to app.ratingButtonActiveColor,
"borderColor" to app.borderColor,
"inputBackground" to app.inputBackground,
"inputTextColor" to app.inputTextColor,
"placeholder" to app.placeholder,
"displayThankYouMessage" to app.displayThankYouMessage,
"thankYouMessageHeader" to app.thankYouMessageHeader,
"thankYouMessageDescription" to app.thankYouMessageDescription,
"thankYouMessageDescriptionContentType" to app.thankYouMessageDescriptionContentType?.value,
"thankYouMessageCloseButtonText" to app.thankYouMessageCloseButtonText,
)
}

Expand Down
9 changes: 9 additions & 0 deletions ios/Classes/PostHogDisplaySurvey+Dict.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
if let submitButtonTextColor = appearance.submitButtonTextColor {
appearanceDict["submitButtonTextColor"] = submitButtonTextColor
}
if let textColor = appearance.textColor {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not check but is Android side consistent with these changes as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react-native, ios, and (now) flutter will all be consistent once this is merged

i didn't think android had any built-in survey UI rendering?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, just the infra that powers the other SDKs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adboio Hey what I mean is that we've made changes to iOS public models to support some new colors in this PR. And that's why we bumped the dependency here to 3.38.0 I assume

Android doesn't have a UI but has those same models which we use in PostHogDisplaySurveyExt.kt which is the counterpart file of this one

Can you validate that when running flutter on an Android device these changes still work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ioannisj mind peeping again? ios and android have been released, bumped both min versions here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, thanx for this. Already approved so all good

appearanceDict["textColor"] = textColor
}
if let descriptionTextColor = appearance.descriptionTextColor {
appearanceDict["descriptionTextColor"] = descriptionTextColor
}
Expand All @@ -78,6 +81,12 @@
if let ratingButtonActiveColor = appearance.ratingButtonActiveColor {
appearanceDict["ratingButtonActiveColor"] = ratingButtonActiveColor
}
if let inputBackground = appearance.inputBackground {
appearanceDict["inputBackground"] = inputBackground
}
if let inputTextColor = appearance.inputTextColor {
appearanceDict["inputTextColor"] = inputTextColor
}
if let placeholder = appearance.placeholder {
appearanceDict["placeholder"] = placeholder
}
Expand Down
4 changes: 2 additions & 2 deletions ios/posthog_flutter.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Postog flutter plugin
s.ios.dependency 'Flutter'
s.osx.dependency 'FlutterMacOS'

# ~> Version 3.32.0 up to, but not including, 4.0.0
s.dependency 'PostHog', '>= 3.32.0', '< 4.0.0'
# ~> Version 3.38.0 up to, but not including, 4.0.0
s.dependency 'PostHog', '>= 3.38.0', '< 4.0.0'

s.ios.deployment_target = '13.0'
# PH iOS SDK 3.0.0 requires >= 10.15
Expand Down
3 changes: 3 additions & 0 deletions lib/src/surveys/models/posthog_display_survey.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,12 @@ class PostHogDisplaySurvey {
submitButtonColor: a['submitButtonColor'] as String?,
submitButtonText: a['submitButtonText'] as String?,
submitButtonTextColor: a['submitButtonTextColor'] as String?,
textColor: a['textColor'] as String?,
descriptionTextColor: a['descriptionTextColor'] as String?,
ratingButtonColor: a['ratingButtonColor'] as String?,
ratingButtonActiveColor: a['ratingButtonActiveColor'] as String?,
inputBackground: a['inputBackground'] as String?,
inputTextColor: a['inputTextColor'] as String?,
placeholder: a['placeholder'] as String?,
displayThankYouMessage: a['displayThankYouMessage'] as bool? ?? true,
thankYouMessageHeader: a['thankYouMessageHeader'] as String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ class PostHogDisplaySurveyAppearance {
this.submitButtonColor,
this.submitButtonText,
this.submitButtonTextColor,
this.textColor,
this.descriptionTextColor,
this.ratingButtonColor,
this.ratingButtonActiveColor,
this.inputBackground,
this.inputTextColor,
this.placeholder,
this.displayThankYouMessage = true,
this.thankYouMessageHeader,
Expand All @@ -28,9 +31,12 @@ class PostHogDisplaySurveyAppearance {
final String? submitButtonColor;
final String? submitButtonText;
final String? submitButtonTextColor;
final String? textColor;
final String? descriptionTextColor;
final String? ratingButtonColor;
final String? ratingButtonActiveColor;
final String? inputBackground;
final String? inputTextColor;
final String? placeholder;
final bool displayThankYouMessage;
final String? thankYouMessageHeader;
Expand Down
106 changes: 86 additions & 20 deletions lib/src/surveys/models/survey_appearance.dart
Original file line number Diff line number Diff line change
@@ -1,61 +1,127 @@
import 'dart:math' show sqrt;

import 'package:flutter/material.dart';
import 'posthog_display_survey_appearance.dart';

/// Appearance configuration for survey widgets
@immutable
class SurveyAppearance {
const SurveyAppearance({
this.backgroundColor,
this.backgroundColor = Colors.white,
this.submitButtonColor = Colors.black,
this.submitButtonText = 'Submit',
this.submitButtonTextColor = Colors.white,
this.descriptionTextColor,
this.ratingButtonColor,
this.ratingButtonActiveColor,
this.descriptionTextColor = Colors.black,
this.questionTextColor = Colors.black,
this.closeButtonColor = Colors.black,
this.ratingButtonColor = const Color(0xFFEEEEEE),
this.ratingButtonActiveColor = Colors.black,
this.ratingButtonSelectedTextColor = Colors.white,
this.ratingButtonUnselectedTextColor = const Color(0x80000000),
this.displayThankYouMessage = true,
this.thankYouMessageHeader = 'Thank you for your feedback!',
this.thankYouMessageDescription,
this.thankYouMessageCloseButtonText = 'Close',
this.borderColor,
this.borderColor = const Color(0xFFBDBDBD),
this.inputBackgroundColor = Colors.white,
this.inputTextColor = Colors.black,
this.inputPlaceholderColor = const Color(0xFF757575),
this.choiceButtonBorderColor = Colors.black,
this.choiceButtonTextColor = Colors.black,
});

final Color? backgroundColor;
final Color backgroundColor;
final Color submitButtonColor;
final String submitButtonText;
final Color submitButtonTextColor;
final Color? descriptionTextColor;
final Color? ratingButtonColor;
final Color? ratingButtonActiveColor;
final Color descriptionTextColor;
final Color questionTextColor;
final Color closeButtonColor;
final Color ratingButtonColor;
final Color ratingButtonActiveColor;
final Color ratingButtonSelectedTextColor;
final Color ratingButtonUnselectedTextColor;
final bool displayThankYouMessage;
final String thankYouMessageHeader;
final String? thankYouMessageDescription;
final String thankYouMessageCloseButtonText;
final Color? borderColor;
final Color borderColor;
final Color inputBackgroundColor;
final Color inputTextColor;
final Color inputPlaceholderColor;
final Color choiceButtonBorderColor;
final Color choiceButtonTextColor;

/// Creates a [SurveyAppearance] from a [PostHogDisplaySurveyAppearance]
static SurveyAppearance fromPostHog(
PostHogDisplaySurveyAppearance? appearance) {
final backgroundColor =
_colorFromHex(appearance?.backgroundColor) ?? Colors.white;
final submitButtonColor =
_colorFromHex(appearance?.submitButtonColor) ?? Colors.black;
final ratingButtonColor =
_colorFromHex(appearance?.ratingButtonColor) ?? const Color(0xFFEEEEEE);
final ratingButtonActiveColor =
_colorFromHex(appearance?.ratingButtonActiveColor) ?? Colors.black;

// Input background: use override, or slight adjustment for high luminance backgrounds
final inputBackgroundColor = _colorFromHex(appearance?.inputBackground) ??
(backgroundColor.computeLuminance() > 0.95
? const Color(0xFFF8F8F8)
: backgroundColor);

// Primary text color: use textColor override if provided, otherwise auto-contrast
final primaryTextColor = _colorFromHex(appearance?.textColor) ??
_getContrastingTextColor(backgroundColor);

// Input text color: use override if provided, otherwise auto-contrast from input background
final inputTextColor = _colorFromHex(appearance?.inputTextColor) ??
_getContrastingTextColor(inputBackgroundColor);

return SurveyAppearance(
backgroundColor: _colorFromHex(appearance?.backgroundColor),
submitButtonColor:
_colorFromHex(appearance?.submitButtonColor) ?? Colors.black,
backgroundColor: backgroundColor,
submitButtonColor: submitButtonColor,
submitButtonText: appearance?.submitButtonText ?? 'Submit',
submitButtonTextColor:
_colorFromHex(appearance?.submitButtonTextColor) ?? Colors.white,
descriptionTextColor: _colorFromHex(appearance?.descriptionTextColor),
ratingButtonColor: _colorFromHex(appearance?.ratingButtonColor),
ratingButtonActiveColor:
_colorFromHex(appearance?.ratingButtonActiveColor),
submitButtonTextColor: _colorFromHex(appearance?.submitButtonTextColor) ??
_getContrastingTextColor(submitButtonColor),
descriptionTextColor:
_colorFromHex(appearance?.descriptionTextColor) ?? primaryTextColor,
questionTextColor: primaryTextColor,
closeButtonColor: primaryTextColor,
ratingButtonColor: ratingButtonColor,
ratingButtonActiveColor: ratingButtonActiveColor,
ratingButtonSelectedTextColor:
_getContrastingTextColor(ratingButtonActiveColor),
ratingButtonUnselectedTextColor: inputTextColor.withAlpha(128),
displayThankYouMessage: appearance?.displayThankYouMessage ?? true,
thankYouMessageHeader:
appearance?.thankYouMessageHeader ?? 'Thank you for your feedback!',
thankYouMessageDescription: appearance?.thankYouMessageDescription,
thankYouMessageCloseButtonText:
appearance?.thankYouMessageCloseButtonText ?? 'Close',
borderColor: _colorFromHex(appearance?.borderColor),
borderColor:
_colorFromHex(appearance?.borderColor) ?? const Color(0xFFBDBDBD),
inputBackgroundColor: inputBackgroundColor,
inputTextColor: inputTextColor,
inputPlaceholderColor: inputTextColor.withAlpha(153),
choiceButtonBorderColor: primaryTextColor,
choiceButtonTextColor: primaryTextColor,
);
}

/// Returns black or white text color based on the perceived brightness of the background.
/// Uses the HSP (Highly Sensitive Perceived) color model for perceived brightness.
/// This matches the algorithm used in posthog-js.
static Color _getContrastingTextColor(Color color) {
final r = color.red;
final g = color.green;
final b = color.blue;
// HSP equation for perceived brightness
final hsp = sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b));
// Using 127.5 as threshold (same as JS)
return hsp > 127.5 ? Colors.black : Colors.white;
}

static Color? _colorFromHex(String? colorString) {
if (colorString == null || colorString.isEmpty) return null;

Expand Down
10 changes: 5 additions & 5 deletions lib/src/surveys/widgets/number_rating_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class NumberRatingButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final buttonColor = isSelected
? appearance.ratingButtonActiveColor ?? Colors.black
: appearance.ratingButtonColor ?? Colors.grey.shade200;
? appearance.ratingButtonActiveColor
: appearance.ratingButtonColor;

return Expanded(
child: Row(
Expand Down Expand Up @@ -61,8 +61,8 @@ class NumberRatingButton extends StatelessWidget {
value.toString(),
style: TextStyle(
color: isSelected
? Colors.white
: Colors.black.withValues(alpha: 0.5),
? appearance.ratingButtonSelectedTextColor
: appearance.ratingButtonUnselectedTextColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
Expand All @@ -78,7 +78,7 @@ class NumberRatingButton extends StatelessWidget {
Container(
height: 45,
width: 1,
color: appearance.borderColor ?? Colors.grey.shade400,
color: appearance.borderColor,
),
],
),
Expand Down
8 changes: 5 additions & 3 deletions lib/src/surveys/widgets/open_text_question.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ class _OpenTextQuestionState extends State<OpenTextQuestion> {
minHeight: 80,
),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade400),
color: widget.appearance.inputBackgroundColor,
border: Border.all(color: widget.appearance.borderColor),
borderRadius: BorderRadius.circular(6),
),
child: SingleChildScrollView(
Expand All @@ -77,10 +77,12 @@ class _OpenTextQuestionState extends State<OpenTextQuestion> {
textAlignVertical: TextAlignVertical.top,
decoration: InputDecoration(
hintText: 'Start typing...',
hintStyle: TextStyle(color: Colors.grey.shade600),
hintStyle: TextStyle(
color: widget.appearance.inputPlaceholderColor),
contentPadding: const EdgeInsets.all(12),
border: InputBorder.none,
),
style: TextStyle(color: widget.appearance.inputTextColor),
onChanged: (value) {
setState(() {
_response = value;
Expand Down
4 changes: 2 additions & 2 deletions lib/src/surveys/widgets/question_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class QuestionHeader extends StatelessWidget {
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
color: appearance.questionTextColor,
),
),
],
Expand All @@ -39,7 +39,7 @@ class QuestionHeader extends StatelessWidget {
description!,
style: TextStyle(
fontSize: 16,
color: appearance.descriptionTextColor ?? Colors.black,
color: appearance.descriptionTextColor,
),
),
],
Expand Down
6 changes: 3 additions & 3 deletions lib/src/surveys/widgets/rating_icons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -445,14 +445,14 @@ enum RatingIconType {
class RatingIcon extends StatelessWidget {
final bool selected;
final RatingIconType type;
final Color? color;
final Color color;
final double size;

const RatingIcon({
super.key,
required this.type,
this.selected = false,
this.color,
required this.color,
this.size = 48.0,
});

Expand All @@ -465,7 +465,7 @@ class RatingIcon extends StatelessWidget {
painter: RatingIconPainter(
selected: selected,
type: type,
color: color ?? Theme.of(context).iconTheme.color ?? Colors.grey,
color: color,
),
),
);
Expand Down
9 changes: 5 additions & 4 deletions lib/src/surveys/widgets/rating_question.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ class _RatingQuestionState extends State<RatingQuestion> {
final range = widget.scaleUpperBound - widget.scaleLowerBound + 1;
if (widget.type == PostHogDisplaySurveyRatingType.emoji &&
(range == 3 || range == 5)) {
final buttonColor =
isSelected ? Colors.black : Colors.black.withAlpha(128);
final buttonColor = isSelected
? widget.appearance.choiceButtonTextColor
: widget.appearance.choiceButtonTextColor.withAlpha(128);
// Convert value to 0-based index.
// When scaleLowerBound is zero (NPS), the index is the same as the value.
final index = value - widget.scaleLowerBound;
Expand Down Expand Up @@ -163,10 +164,10 @@ class _RatingQuestionState extends State<RatingQuestion> {
else
Container(
decoration: BoxDecoration(
color: Colors.white,
color: widget.appearance.inputBackgroundColor,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: widget.appearance.borderColor ?? Colors.grey.shade400,
color: widget.appearance.borderColor,
width: 2,
),
),
Expand Down
Loading