feat(app): onboarding that ends in a first conversation (A/B), paywall re-ask on app open (A/B), Home live pill

Draft
YI
1d ago
+17437-9281

Draft. Behind a PostHog A/B flag (onboarding-activation-flow, not created yet), so nothing changes for users until it's turned on. Every paywall goes through Nik's paywall experiment (#21450); the Superwall pilot is opt-in, iOS debug only.

Why

Device buyers who use Omi in their first days subscribe; most who don't subscribe barely use it and never see a paywall. The voice intro is the biggest drop inside onboarding. So: one clear use moment with a visible result, then a paywall for everyone, then a Home that isn't empty.

What changed

Onboarding (both arms)

  • Opens straight on sign-in (no Get Started). Name opens with the keyboard; simpler permission toggles; language after permissions; attribution question removed.
  • New Connect Omi step: pair in-page, or "I've ordered mine" / "Not ordered yet" (opens the store). The answer is tracked (Onboarding Device Choice), so we can finally see buyers we can't match by email.
  • An Omi connected here gets the button tutorial right away.

New ending (activation arm)

  • "Let's try Omi" → read three short sentences → live words → real summary and to-dos → "You're all set" → rating → paywall → Home.
  • Phone dictation also saves the voice profile. Never stuck: silence or a failed start shows a clear Continue.

Home and Settings

  • Bottom bar: Ask Omi + a live recording pill (Listening, newest words; tap for the transcript).
  • First conversation and its to-dos (or starter tasks) waiting on Home; Omi's first chat message.
  • Settings shows minutes and an Upgrade button directly (before: Settings → Plan → Upgrade).

Device-test fixes (Oct 14)

From a run on a real iPhone with live transcription:

  • Dictation: read-along — the script lights up word by word with the voice (soft tick per word, waits on silence); Omi's sentences confirm it, or the person's own words ink in when they said something else. A live waveform shows Omi is listening.
  • Summarizing: the words condense in the same card (filler folds away, key words draw together); the sparkle became three lines condensing into one. The card no longer jumps between the two steps.
  • Summary: rises out of a blur; "Omi remembers so you don't have to" above the title; no "still writing your notes" line.
  • Rating: moves on to the paywall under the App Store pop-up (iOS gives no close callback, so the screen used to stay up).
  • Home: fades in after onboarding (a slide showed Home over Home).
  • Buttons: every new-flow step's primary button sits 24pt above the home bar.
  • Pull-to-refresh (Home, Tasks, iOS): a ring draws as you pull, ticks when armed, then spins.

A/B setup

  • PostHog: draft experiment 482367 on flag onboarding-activation-flow (inactive until launched, so everyone gets control). 50/50 control (today's voice intro) / test (new ending), user-based.
  • Exposure: the app turns off $feature_flag_called, so the experiment counts the custom event Onboarding Ending Shown, which now carries $feature/onboarding-activation-flow. People who fell back to control (no identity, slow read, error) carry no variant and stay out of the analysis.
  • The arm is read at Connect Omi and saved per account, so a resumed onboarding keeps its arm.
  • Primary metric: subscribed within 30 days. Subscriptions live in Stripe data, not app events, so the metric still has to be added (data owner's call). Guardrail: onboarding completion can't drop more than 2 points. Pre-registration: app/lib/services/experiments/onboarding_activation_flow.md.
  • Launch only after a store build with this code ships.

Paywalls: built on #21450

Every upgrade entry point (end of the activation, Settings Upgrade, Plan & Usage, locked conversations and memories, chat quota, Plus meter) calls one helper, openUpgradePaywall: Superwall pilot when it's on → otherwise Nik's PaywallRouter (flag paywall-variant: control / outline / web) → otherwise the existing plans sheet. Paywall Opened is logged once per open; the web variant's fallback (forceNative) skips the router so it can't loop.

  • End of onboarding: the activation ending's paywall asks the router first (so its arms and exposure count), then falls back to the onboarding plans sheet. The experiment's own end-of-onboarding placement is skipped when the ending already offered a plan, so nobody sees two paywalls in a row.
  • Nik's web onboarding entry and debug onboarding rerun (OMI_ONBOARDING_RERUN=1) are kept.

Paywall re-ask on app open (A/B, flag paywall-reask)

Free users keep opening the app but rarely meet a paywall, because every paywall today waits for a limit. On launch or return to the app, a free user whose re-ask is due gets the usual paywall (same helper, so paywall-variant still picks the design), queued in PromptQueue so it never covers a recording, a call or another prompt.

  • Schedule: 2, 3, 5, then every 7 calendar days after the last paywall seen from any source (a limit or Settings Upgrade restarts the clock). The first Home visit only starts the clock, so never on onboarding day; at most once a day; dropped if they subscribed meanwhile.
  • A/B: control / test. Both arms are exposed ($feature_flag_called) at the same due moments; control shows nothing. New Paywall Opened source app_open_reask. Pre-registration: app/lib/services/experiments/paywall_reask.md (primary: Upgrade Succeeded within 14 days; guardrail: active days can't drop >5%).
  • Not yet created in PostHog; until it is, nobody is in the experiment and nothing shows.

Superwall pilot (iOS debug, opt-in)

Named placements: device_setup_complete (end of onboarding), settings_upgrade, plus_meter, locked_content, usage_upgrade, chat_apps, chat_quota, transcription_settings_usage. Off unless a debug build passes --dart-define=SUPERWALL_TEST_MODE=true; anywhere it's off, the paywall experiment decides.

For production: Omi's own Superwall project (replaces the test key) with campaigns for these placements; the plans as in-app purchases in App Store Connect; backend reconciliation of purchases. See app/docs/superwall-pilot.md.

How to try it

flutter run --flavor dev (debug), as a new user: new ending + the paywall experiment's arm (control = plans sheet) by default.

  • --dart-define=OMI_ONBOARDING_ENDING=control: old ending
  • --dart-define=OMI_ACTIVATION_DEMO=1: scripted words on a simulator
  • --dart-define=OMI_PAYWALL_VARIANT=outline (or web): force a paywall arm
  • --dart-define=SUPERWALL_TEST_MODE=true: Superwall pilot paywall instead
  • --dart-define=OMI_PAYWALL_REASK=test --dart-define=OMI_PAYWALL_REASK_NOW=true: re-ask on every app open

Risks / open

  • #21426 left out on purpose: its screens are replaced or removed here. Its pairing errors could come to Connect Omi later.
  • Paywall ownership: main's paywall work (#21354, #21397, #21450) is merged in; every entry point now goes through #21450's router. Superwall stays a debug-only pilot until Omi decides on its billing path.
  • Rating ask inside onboarding (Guideline 5.6.3); attribution data lost; voice profile only from phone dictation.
  • Still to test on device (Yinka): transcription in onboarding and the button tutorial after Connect Omi. The dictation, summary and to-dos have only run with scripted words (OMI_ACTIVATION_DEMO), because the local server has no speech-to-text. Next: a real phone run against a backend with live transcription.
  • Not yet verified: real Omi pairing (and the button-tutorial handoff), real StoreKit prices.

Verified

  • On the merge with main (incl. #21450) + re-ask: flutter test test/widgets test/providers test/unit test/spine: 4,418 passed (1 skipped, as on main) before the re-ask; full suite on the re-ask commit: 4,425 passed (1 skipped); re-ask tests re-run after the schedule change: pass.
  • scripts/pr-preflight --lane local: 31/31 checks pass.
  • iOS simulator (iPhone 17 Pro, local server): activation → Outline paywall via the router; control → onboarding plans sheet → Home with no second paywall; Settings → Upgrade → Outline; re-ask on returning to the app; a plain build doesn't re-ask.
Screenshots (iOS simulator, debug build with scripted dictation) Sign-in over the launch film, no Get Started screen (OMI_ONBOARDING_HERO=video; local test build shows one local sign-in button where store builds show Apple/Google) Data & Privacy over the launch film Name, focused and ready to type Permissions as simple toggle cards, both answered Language, pre-selected from the phone Connect Omi with I've ordered mine / Not ordered yet Activation intro: Let's try Omi Microphone ask: the mic control the dictation uses Dictation: the user's words appear live Summarizing, with the user's words The first conversation's summary To-dos found in it, saved to Tasks You're all set Rating ask with the App Store prompt Opt-in Superwall pilot paywall at the end of onboarding (SUPERWALL_TEST_MODE=true; mock products, so prices are blank) Home after onboarding: the first conversation and a dot on Tasks Omi's first chat message in an empty chat Bottom-bar live pill while the phone records (local server has no transcription, so the Reconnecting card also shows) Control arm: today's guided voice introduction, unchanged End of the activation through the router, Outline arm (local server has no plan catalog) End of the activation, control: the onboarding plans sheet Paywall re-ask on returning to the app Settings Upgrade through the router, Outline arm
Repo checks

Product invariants affected

  • INV-DATA-1: main.dart only adds SuperwallPilot.configure() / syncUser(uid); routing unchanged, pilot inert outside iOS debug.
  • INV-MEM-1: memory_item.dart routes the locked-memory upgrade through openUpgradePaywall (same source); storage unchanged.
  • INV-CAP-1: capture_controller.dart adds an optional phoneMicAudioTap that gets a copy of phone-mic PCM; never consumes, delays or drops audio.

Line-count exceptions

Line-Count-Exception: app/lib/backend/preferences.dart | 1514 -> 1519 | one preference for the closable Home voice-profile card
Line-Count-Exception: app/lib/providers/device_provider.dart | 1488 -> 1504 | hold the firmware prompt during first-run onboarding and show it on Home
Line-Count-Exception: app/lib/utils/analytics/analytics_manager.dart | 3131 -> 3138 | one onboarding device-choice event for matching buyers to orders
Line-Count-Exception: app/lib/pages/settings/transcription_settings_page.dart | 1554 -> 1559 | Superwall placement name on the existing usage-page route

Failure class (fixes)

Failure-Class: none

🤖 Generated with Claude Code