feat(desktop): add 'How did you hear about us?' onboarding step#6234
Conversation
|
Mac mini test: FAIL Build error — duplicate
Fix: Remove the Code review of the onboarding step itself looks correct (step at index 2, wired in OnboardingView.swift, analytics tracking, chip shuffle). The only issue is the duplicate struct. |
Greptile SummaryThis PR adds a "How did you hear about Omi?" onboarding step at index 2 (between Language and Trust), with randomized chip selection, immediate advancement on tap, and K-factor analytics events sent to PostHog, Mixpanel, and Heap. It also fixes floating bar follow-up auto-focus on conversation restore. The step wiring and index renumbering in Key findings:
Confidence Score: 4/5Safe to merge after fixing the migration order in OnboardingFlow.swift; remaining findings are minor analytics and performance suggestions. One P1 migration ordering bug exists that will misplace a narrow but real subset of users onto the wrong onboarding step. The fix is a two-block swap with no other changes required. The remaining two items are P2 style/performance suggestions that do not block correctness. desktop/Desktop/Sources/OnboardingFlow.swift — the order of the hasInsertedHowDidYouHearStep and hasReorderedTrustStep migration blocks must be swapped. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["OnboardingView.onAppear"] --> B["OnboardingFlow.migratedStep()"]
B --> C{"prior migrations..."}
C --> O{"!hasInsertedHowDidYouHearStep? ⚠️"}
O -- Yes --> P["migratedStep += 1 (if ≥2)"]
O -- No --> Q{"!hasReorderedTrustStep\n&& hasMigratedPagedIntro? ⚠️"}
P --> Q
Q -- Yes --> R["Reorder: 0→2, 1→0, 2→1\nBUG: runs after HowDidYouHear shift"]
Q -- No --> S["return clamp(migratedStep, 0, lastStepIndex)"]
R --> S
S --> T["currentStep updated; all flags set to true"]
style O fill:#ff9999,stroke:#cc0000
style Q fill:#ff9999,stroke:#cc0000
style R fill:#ff9999,stroke:#cc0000
|
| Old step | After HowDidYouHear insertion | After Trust reorder | Expected |
|---|---|---|---|
| 0 (Trust) | 0 (< 2, unchanged) | case 0 → 2 → HowDidYouHear |
3 (Trust) |
| 1 (Name) | 1 (< 2, unchanged) | case 1 → 0 → Name |
0 (Name) ✓ |
| 2 (Language) | 3 (shifted up) | no case match → Trust | 1 (Language) |
The fix is to run the Trust reorder migration before the HowDidYouHear insertion:
// Only reorder for existing users who already had the old Trust-first layout.
// Must run BEFORE the HowDidYouHear insertion so index offsets compose correctly.
if !hasReorderedTrustStep && hasMigratedPagedIntro {
switch migratedStep {
case 0:
migratedStep = 2
case 1:
migratedStep = 0
case 2:
migratedStep = 1
default:
break
}
}
// HowDidYouHear step inserted at index 2; shift users at 2+ up
if !hasInsertedHowDidYouHearStep, migratedStep >= 2 {
migratedStep += 1
}Reviews (1): Last reviewed commit: "chore(desktop): add changelog entry for ..." | Re-trigger Greptile
| func onboardingHowDidYouHear(source: String) { | ||
| let props: [String: Any] = ["source": source, "is_referral": source == "Friend"] | ||
| let mixpanelProps = props.compactMapValues { $0 as? MixpanelType } | ||
| MixpanelManager.shared.track("Onboarding How Did You Hear", properties: mixpanelProps) | ||
| PostHogManager.shared.track("Onboarding How Did You Hear", properties: props) | ||
| HeapManager.shared.track("Onboarding How Did You Hear", properties: ["source": source, "is_referral": "\(source == "Friend")"]) | ||
| } |
There was a problem hiding this comment.
is_referral type inconsistency across analytics platforms
PostHog receives is_referral as a native Bool, while Heap receives it as the string "true" or "false". This makes cross-platform funnel comparisons error-prone — a query filtering on is_referral = true in PostHog won't match is_referral = "true" in Heap.
func onboardingHowDidYouHear(source: String) {
let isReferral = source == "Friend"
let props: [String: Any] = ["source": source, "is_referral": isReferral]
let mixpanelProps = props.compactMapValues { $0 as? MixpanelType }
MixpanelManager.shared.track("Onboarding How Did You Hear", properties: mixpanelProps)
PostHogManager.shared.track("Onboarding How Did You Hear", properties: props)
HeapManager.shared.track("Onboarding How Did You Hear", properties: ["source": source, "is_referral": isReferral])
}| /// Simple flow layout that wraps chips to the next line when they exceed width. | ||
| struct FlowLayout: Layout { | ||
| var spacing: CGFloat = 10 | ||
|
|
||
| func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { | ||
| let result = arrange(proposal: proposal, subviews: subviews) | ||
| return result.size | ||
| } | ||
|
|
||
| func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { | ||
| let result = arrange(proposal: proposal, subviews: subviews) | ||
| for (index, position) in result.positions.enumerated() { | ||
| subviews[index].place( | ||
| at: CGPoint(x: bounds.minX + position.x, y: bounds.minY + position.y), | ||
| proposal: .unspecified | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private func arrange(proposal: ProposedViewSize, subviews: Subviews) -> (size: CGSize, positions: [CGPoint]) { | ||
| let maxWidth = proposal.width ?? .infinity | ||
| var positions: [CGPoint] = [] | ||
| var x: CGFloat = 0 | ||
| var y: CGFloat = 0 | ||
| var rowHeight: CGFloat = 0 | ||
| var totalHeight: CGFloat = 0 | ||
|
|
||
| for subview in subviews { | ||
| let size = subview.sizeThatFits(.unspecified) | ||
| if x + size.width > maxWidth && x > 0 { | ||
| x = 0 | ||
| y += rowHeight + spacing | ||
| rowHeight = 0 | ||
| } | ||
| positions.append(CGPoint(x: x, y: y)) | ||
| rowHeight = max(rowHeight, size.height) | ||
| x += size.width + spacing | ||
| totalHeight = y + rowHeight | ||
| } | ||
|
|
||
| return (CGSize(width: maxWidth, height: totalHeight), positions) | ||
| } | ||
| } |
There was a problem hiding this comment.
FlowLayout.arrange is computed twice per layout pass
sizeThatFits and placeSubviews both call arrange(proposal:subviews:), meaning the full chip-layout algorithm runs twice every time SwiftUI performs a layout pass. The Layout protocol's cache mechanism exists specifically for this purpose. Consider using a typed cache:
struct FlowLayout: Layout {
var spacing: CGFloat = 10
struct CacheData {
var size: CGSize
var positions: [CGPoint]
}
func makeCache(subviews: Subviews) -> CacheData {
CacheData(size: .zero, positions: [])
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) -> CGSize {
let result = arrange(proposal: proposal, subviews: subviews)
cache = CacheData(size: result.size, positions: result.positions)
return result.size
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) {
for (index, position) in cache.positions.enumerated() {
subviews[index].place(
at: CGPoint(x: bounds.minX + position.x, y: bounds.minY + position.y),
proposal: .unspecified
)
}
}
}|
Mac mini test: PASS - build succeeded, code verified correct
|
New step after Language (index 2) with randomized source options. Tracks "Friend" selections with is_referral flag for K-factor analysis. Sends events to PostHog, Mixpanel, and Heap. Includes migration for existing users to skip past the new step. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add step at index 2, bump introStepCount to 13, add migration to shift existing users past the new step. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Insert step at index 2, bump all subsequent step indices by 1, add migration flag for existing users. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sends source and is_referral flag to PostHog, Mixpanel, and Heap for K-factor tracking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FlowLayout already exists in AppsPage.swift. Use empty string for description since OnboardingStepScaffold doesn't accept optional. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6a273ff to
3417ce5
Compare
…dHardware#6234) ## Summary - Adds a new onboarding step (index 2, after Language) asking "How did you hear about Omi?" - Options: Social media, YouTube, Newsletter, AI chat, Search engine, Event, **Friend**, Colleague, Podcast, Article, Product Hunt, Other - Options are **randomized** on every display - Clicking a chip immediately advances (no Continue button needed) - Sends `Onboarding How Did You Hear` event to PostHog, Mixpanel, and Heap with `source` and `is_referral` (true when "Friend" is selected) for **K-factor tracking** - Includes migration so existing users skip past the new step - Uses existing `OnboardingStepScaffold` + `OnboardingSelectableChip` design components ## Test plan - [ ] Fresh onboarding: after Language step, "How did you hear about Omi?" appears with randomized chips - [ ] Clicking any chip advances to the Trust step - [ ] Options are in different order each time the step is shown - [ ] Existing users who already passed onboarding are not affected (migration skips them) - [ ] Analytics event fires with correct source value 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
Onboarding How Did You Hearevent to PostHog, Mixpanel, and Heap withsourceandis_referral(true when "Friend" is selected) for K-factor trackingOnboardingStepScaffold+OnboardingSelectableChipdesign componentsTest plan
🤖 Generated with Claude Code