feat(surveys): add posthog-android-surveys-compose default UI module#541
Draft
leonhardprinz wants to merge 14 commits into
Draft
feat(surveys): add posthog-android-surveys-compose default UI module#541leonhardprinz wants to merge 14 commits into
leonhardprinz wants to merge 14 commits into
Conversation
Adds a new optional Gradle module `posthog-android-surveys-compose` that ports the iOS SwiftUI survey UI (PostHog/posthog-ios#320) to Jetpack Compose. Closes the long-standing gap in #102: the SDK's default `PostHogSurveysDelegate` only logs, so customers have had to ship their own UI. Driven by Fujifilm (20+ INSTAX apps) who are blocked rolling out NPS surveys on Android. Disclosure: vibe-coded by a TAM using PostHog Code with the iOS implementation as the port-from blueprint. MVP scope (this commit): - NPS / Number Rating question type only (0–10, 1–5, 1–7 scales) - Material 3 ModalBottomSheet container with `confirmValueChange` intercept so only the X button dismisses (matches iOS `interactiveDismissDisabled` semantics) - Theming sourced from PostHogDisplaySurveyAppearance — faithful port of the iOS hex/CSS-name color parser (140-entry color table) - onSurveyShown / onSurveyResponse / onSurveyClosed callbacks wired so the core SDK fires `survey shown` / `survey sent` / `survey dismissed` events correctly - ComposeView injected into the foreground Activity's android.R.id.content via an ActivityLifecycleCallbacks-backed ActivityProvider — works on any Activity type - @Preview composables (default + themed) - Sample app wired with `Trigger test survey` button capturing `show_test_survey` Out of MVP (planned follow-ups): open-text/choice/link questions, emoji rating, thank-you screen, multi-question branching coverage, dark-mode polish, Compose UI tests, accessibility audit. Architecture: - Separate Gradle module — non-Compose customers don't pay APK-size cost. Mirrors how AndroidX ships optional Compose modules. - Independent versioning starting at 1.0.0-alpha01 — lets the module iterate through alphas without dragging core SDK versions. - Compose BOM 2024.12.01 (same as the sample app). - No changes to `:posthog` or `:posthog-android` modules; no public API changes; module excluded from binary-compatibility-validator during alpha. Refs: #102 Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
Android Lint's AutoboxingStateCreation rule (warningsAsErrors enabled) flagged mutableStateOf(0) for the question index. Switching to mutableIntStateOf avoids the boxing and clears the lint failure. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
The previous commit removed the mutableStateOf import while switching the question index to mutableIntStateOf, but the nullable rating state at SurveySheet.kt:189 (mutableStateOf<Int?>(null)) still needs the boxed variant since mutableIntStateOf does not support nullable Int. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
AGP aborts the build when lint is configured with a baseline file that doesn't exist on disk (it generates one and exits to force a check-in). Matching the empty placeholder convention used by posthog-android/. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
Adds the appearance fields needed by the remaining question types and the thank-you screen: placeholder text + color, derived questionTextColor and placeholderTextColor, and the displayThankYouMessage / thankYouMessage* fields. Defaults track iOS's getAppearanceWithDefaults — empty fallbacks match "Thank you for your feedback!", "Close", "Start typing...", etc. QuestionHeader now reads questionTextColor (derived from background's contrasting color) rather than textColor, mirroring iOS's backgroundColor.getContrastingTextColor() behavior. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
Multi-line text input for open-ended questions. Visual port of iOS OpenTextQuestionView: 150 dp bordered card with a placeholder rendered behind the BasicTextField while empty. Pulls placeholder text and color from the resolved survey appearance. Component is stateless — caller hoists the text value and onValueChange callback. Two @previews show default and themed appearances side-by-side. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
Single-selection choice list. Visual port of iOS SingleChoiceQuestionView backed by a shared ChoiceOptions composable that also serves the multiple choice port — each option is a rounded bordered button that turns bold and gets a checkmark decoration when selected. Open-choice handling: when question.hasOpenChoice is true the last entry becomes an inline-editable "other" option that opens a BasicTextField once selected. State (selectedChoice and openChoiceInput) is hoisted so the dispatcher can drive submit validation. Two @previews demonstrate default and themed appearances. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
Multi-selection choice list. Delegates to the shared ChoiceOptions composable with multi-selection semantics, mirroring iOS MultipleChoiceQuestionView. State is hoisted as Set<String> plus an openChoiceInput String for the "other" option. Two @previews cover default and themed appearances. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
Emoji rating control rendered with 3 (Dissatisfied/Neutral/Satisfied) or 5 face shapes (VeryDissatisfied → VerySatisfied). All five SVG paths are 1:1 ports of the Shape implementations in iOS Resources.swift — same normalised coordinate space, same y-translation, same fill-mode for the ring outline. We deliberately don't fall back to Unicode emoji: OEM emoji rendering varies dramatically across Android devices and would break visual parity with iOS. The trade-off is ~250 lines of path-construction code; a shared addFaceCircle helper plus dot-eye helpers cuts the per-emoji body to its unique mouth/eye sub-paths. Two @previews cover the default (5-emoji NPS-style) and themed (3-emoji pastel) appearances. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
Body content for link questions plus an openLink helper that fires an
ACTION_VIEW intent on submit. Visual port of iOS LinkQuestionView, which
renders no body content between the header and the submit button. We
additionally surface the destination URL as description-coloured text so
users have context for the action — the iOS-mirrored response payload
("link clicked") is unaffected.
ACTION_VIEW failures (no browser installed) are swallowed; the survey
flow continues either way. Two @previews cover default and themed
appearances.
Generated-By: PostHog Code
Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
Thank-you screen displayed after the last question of a survey when the customer has displayThankYouMessage enabled in their PostHog appearance config. Visual port of iOS ConfirmationMessage: bold header, optional plain-text description (HTML deferred to a follow-up — iOS skips it too), and a close button. Two @previews show default and themed appearances (the themed one includes a description string). Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
Replaces the unsupported-type placeholder with full dispatch over every PostHogDisplaySurveyQuestion subtype. Each dispatcher owns its per- question state (text, rating, choice selection, open-choice input) keyed by question.id so navigating to a new question resets it. When the host SDK reports completion the sheet either shows the ConfirmationScreen (if displayThankYouMessage is set) or dismisses. Otherwise it naïvely advances to currentQuestionIndex + 1 — server- driven branching stays a tracked follow-up. LinkQuestion submission opens question.link via the openLink helper before firing the "link clicked" response. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
README documents the one-line integration, the seven supported question types, and the known gaps (branching, event dispatch, HTML descriptions, Compose UI tests, dark mode). ARCHITECTURE captures the rationale behind the separate module, the ActivityProvider + ComposeView injection approach, state ownership in SurveySheet, appearance resolution, the EmojiRating SVG-path choice, and the deliberately tight responsibility boundary. CHANGELOG moves the new question types from "out of scope" into the shipped feature list. PostHogSurveysComposeDelegate KDoc updates the "MVP scope" blurb to reflect what's now covered. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
…ierTo Compose UI's `Path.quadraticBezierTo` was deprecated in favor of `quadraticTo` (for consistency with `cubicTo`). The new name compiles cleanly under `-Werror`. Generated-By: PostHog Code Task-Id: b89cbdd1-9f16-4fa4-b3d0-3f9b268b0c2b
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Closes the long-standing gap in #102 (open since Feb 2024): the Android SDK's default surveys delegate is a stub that only logs. Customers using surveys on Android have to implement their own UI, which is a significant lift.
Driven by large customer (20+ global apps), who are blocked rolling out post-print NPS surveys on Android. iOS shipped a complete SwiftUI implementation in PostHog/posthog-ios#320 — this PR ports the same pattern to Kotlin/Compose as an opt-in module so non-Compose apps don't pay for it.
What's in this PR
:posthog-android-surveys-compose— Material 3ModalBottomSheet+ Jetpack Compose@PreviewcomposablesdisplayThankYouMessage, otherwise the sheet dismisses immediatelyPostHogDisplaySurveyAppearance— background, submit button, text, border, rating-button, placeholder, and thank-you copy colors all pull through, plus a faithful port of the iOS hex / CSS-color name parser (140-entry table)onSurveyShown/onSurveyResponse/onSurveyClosedfire at the right moments so the SDK'ssurvey shown/survey sent/survey dismissedevents fire correctly. The delegate never callsPostHog.capturedirectly.ComposeViewinjected into the foreground Activity'sandroid.R.id.contentvia anActivityLifecycleCallbacks-backedActivityProvider— works on any Activity type (noFragmentActivityrequirement)Resources.swiftSVG paths — same normalised coordinate space, same fill mode, same y-translation. Unicode emoji was deliberately rejected because OEM rendering varies dramatically across Android devices.README.md+ARCHITECTURE.md— one-line integration, supported question types, known gaps; full rationale for the separate module, ComposeView injection, state ownership, appearance resolution, and the EmojiRating path choiceTrigger test surveybutton capturingshow_test_survey, delegate set inMyApp.ktWhat's NOT in this PR (planned follow-ups)
currentQuestionIndex + 1)Architecture decisions
:posthog-android, no changes to:posthogor:posthog-android) — non-Compose apps don't pay APK-size cost. Mirrors the AndroidX optional-Compose-module pattern.1.0.0-alpha01— lets this module iterate through alphas without dragging core SDK versions. Module added tobinary-compatibility-validatorignoredProjectsduring alpha.2024.12.01(matches the sample app's existing pin), Material 31.3.1,kotlin.plugin.composefor K2-era Compose.rememberModalBottomSheetState(confirmValueChange = { it != SheetValue.Hidden })for the "X-button-only dismissal" — matches iOSinteractiveDismissDisabled()semantics with one line instead of a customSheetState.consumer-rules.pro+-keep PostHogSurveysComposeDelegate— minimal consumer-side ProGuard surface; populate if downstream minified builds ever surface issues.@file-levelktlint_function_naming_ignore_when_annotated_with = Composablein module.editorconfigso PascalCase@Composablefunctions don't trip thestandard:function-namingrule.For Ioannis (next steps)
Resources.swift), andSurveyAppearanceproperty names so customer appearance config in the PostHog UI works on Android without changesrefactor: align SurveyAppearance→feat: OpenText→feat: SingleChoice + ChoiceOptions→feat: MultipleChoice→feat: EmojiRating→feat: LinkQuestion→feat: ConfirmationScreen→feat: wire dispatch in SurveySheet→docs: README + ARCHITECTURE)survey shown/survey sent/survey dismissed)Local verification
This is what I ran in my dev env (no Android emulator available there, so the visual checks are deferred to a real device run):
./gradlew spotlessCheck— passes./gradlew detekt— passes./gradlew :posthog:build— passes (confirming no regression in core):posthog-android-surveys-compose:assembleDebug,:posthog-android-surveys-compose:lint, full./gradlew build) deferred to CI here because my dev env doesn't have the Android SDKManual test plan (for me to run on an emulator before un-drafting)
show_test_survey, set custom background/submit colors so theming is visiblesurvey sentwith the right rating, X-button firessurvey dismissed, swipe-down is ignoredReviewers
cc @marandaneto @ioannisj @lucasheriques
Will also link the Feb 2026 internal Slack thread on Android surveys in a comment.
Refs #102