Skip to content

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

Closed
YinkaAdepoju wants to merge 81 commits into
BasedHardware:mainfrom
YinkaAdepoju:yinka/onboarding-superwall
Closed

YinkaAdepoju wants to merge 81 commits into
BasedHardware:mainfrom
YinkaAdepoju:yinka/onboarding-superwall

Conversation

@YinkaAdepoju

@YinkaAdepoju YinkaAdepoju commented Oct 13, 2026 •

Copy link
Copy Markdown

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

Verified

  • On the merge with main (incl. feat(app): PostHog paywall A/B test (control / outline / web) + flag-gated web onboarding #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

Yinka and others added 30 commits October 12, 2026 14:29
Free and Plus users see this month's minutes and an Upgrade button under
Plan & Usage, so upgrading is one tap from Settings. Free users route
through the Superwall settings_upgrade placement when the local pilot is
on; everyone else gets the existing plans sheet. Extracts the standalone
plans sheet wrapper so Settings and the Plus meter prompt share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tapping locked content now shows the locked_content paywall (Superwall
placement when the local pilot is on, otherwise the plans sheet) over the
current screen instead of pushing Plan & Usage first. Locked memories log
Paywall Opened with source 'Locked Memory' instead of the mislabelled
'Action Item'. Adds a shared openUpgradePaywall helper used by Settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a free user reaches All set on their first device setup, register
the Superwall device_setup_complete placement. Superwall only, with no
plans-sheet fallback, so nothing changes when the local pilot is off.
Skipped tutorials and replays from Settings are excluded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finished, skipped and replayed tutorials all close through the same path,
which registers the Superwall placement for anyone not subscribed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plan & Usage takes the placement and source for the offer it opens on
arrival (usage_upgrade by default; transcription_settings_usage from
Transcription Settings, chat_apps from Chat Apps, Deep Link source for the
unlimited link). Chat quota (chat_quota) and the Plus meter (plus_meter)
go through the shared openUpgradePaywall helper, which replaces the two
private plans-sheet wrappers. Existing flows are unchanged when the local
Superwall pilot is off; each entry now logs Paywall Opened with its source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ask for Bluetooth as the app opens and search for the reader's Omi in
  the background after sign-in
- Name opens with the keyboard up; no artwork after consent
- Remove the "How did you find Omi" step
- Permissions as switch cards; Continue unlocks once every card is
  answered (denied shows orange, notifications red with Tap to fix)
- Language moves after permissions
- New Connect your Omi step: reveal when found, search with
  instructions otherwise, and "I've ordered mine / Not ordered yet"
- A device connected in onboarding skips the post-onboarding tutorial
- Progress dots become a single line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Shared plain step layout (title top, action pinned above keyboard);
  Name centres its field and keeps Continue working on the first tap
- Permissions as separate cards with a custom toggle that carries the
  state colour (grey unanswered, accent on, orange/red with "!" denied)
- Language uses the same layout
- Connect Omi: clearer hierarchy, even text wrap, quiet links for
  "I've ordered mine / Not ordered yet"; rings follow the theme
- Progress line level with the back button
- Direction-aware slide and fade between steps (off with Reduce Motion)
- Quiet device search only when Bluetooth is allowed and on, so no
  "Enable Bluetooth" alert appears over unrelated steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Permissions cards and the language picker sit centred like Name
- Language title "Choose your language" (one line)
- No back button or swipe-back on Connect Omi (settings stay editable
  later); arrives with a cross-fade, then the device rises in with a
  light tap and the text fades up in sequence
- Bluetooth check waits for the arrival, so no alert covers it
- Shorter Bluetooth alert: "Turn on Bluetooth to connect your Omi."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Onboarding Device Choice was only an event. Also set it as a person property,
so people who say they ordered can be compared with Shopify orders later
without asking them to link an order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Onboarding's Connect Omi and the in-app pairing page share the pairing
provider. Leaving Connect Omi now ends the search and clears "nothing
found", so the in-app page cannot open on a stale "No Omi Found".
Adds tests for the step: title, deferred search, the two "no Omi yet"
answers, a found device row, and the clean hand-off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Not ordered yet" on Connect Omi now opens omi.me in a clean in-app web
page (the existing OmiWebPage), tagged _ref=onboarding_not_ordered and
logged as Omi Shop Opened; closing it carries on with onboarding.
openOmiShop is reusable for later hardware upsell entry points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Two short lines: storage + AI notes, then "AI processing: Deepgram,
  OpenAI" in small text (Apple 5.1.2(i) still names who receives data)
- Privacy Policy and Terms as small links
- "Use a Different Account" moves above the main action so Agree &
  Continue sits at the same thumb height as every other step
- The card rises from the bottom over the sign-in background

The old long message stays in the ARB files for now; the provider list
needs confirming with the Omi team.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… opens as a sheet with Close

- Data & Privacy: button says Continue and sits where every Continue
  does; "By continuing, you agree to our Terms of Use and Privacy
  Policy." sits under it in the space the other steps leave free.
  "Use a Different Account" removed from this screen.
- OnboardingCard can leave the bottom inset to its footer.
- The Omi store opens as a full-screen sheet with Close (x) instead of
  Back; OmiWebPage gains an opt-in close mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sation, congrats

- Device users: "Wear your Omi"; everyone: "Let's see Omi at work."
  (auto), two animated slides (notes, tasks), microphone ask for phone
  users only
- "Tell Omi about your day": faded example, live words in a centred
  note card, Skip top right, one round check (grey until Omi hears
  something) with listening rings; records through the Omi when it is
  streaming, else the phone microphone
- The dictation goes through the capture pipeline and becomes a real
  conversation (first on Home); "Writing your notes..." then the summary
  and tasks animate in; falls back to "Here's what you said"
- Congrats with name and rating ask, then the plans sheet, then Home
- Skipping shows a dismissible "Welcome to Omi" card on Home
- No back or progress line during the activation
- Debug-only OMI_ACTIVATION_DEMO=1 simulates speech for servers without
  speech-to-text
- 29 new strings in 49 locales

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop the two explainer slides; "Let's try Omi." leads straight to the
  microphone ask (no device) or the dictation.
- Order after the reveal: hooray, rating ask, paywall, Home.
- Rating: title and five stars above where the App Store prompt opens;
  no sub-copy.
- Hooray: title above the check; check and confetti follow.
- Microphone and dictation titles sit lower, level with each other and
  clear of the system prompt; Skip is grey.
- Back button is the bare icon; shorter consent copy.
- Activation flow tests for the full path, skip and fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The dictation turns into a full note: the card stays in place while
  "Summarizing…" sweeps across the words, then the emoji, title and
  "Just now" land, a rule draws across, the summary writes itself and
  the transcript waits behind a toggle.
- To-dos slide in one by one with a light tap each, then "Saved to your
  tasks".
- Every titled activation screen (wear, microphone, dictation,
  summarizing, summary, to-dos, hooray, rating) shares one title size
  and height.
- New copy: "Your day, summarized", "Omi caught your to-dos",
  "Summarizing…"; a fuller demo summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dictation through a connected Omi waits up to 5s for it to stream. If
  it does not, the reader is asked for the phone microphone first (with
  a line saying why) instead of a cold system prompt mid-dictation, and
  the recording is handed back to the Omi afterwards.
- Audio the Omi heard before the dictation (unboxing, the screens before)
  is closed as its own conversation first, and the result shown is the
  conversation whose transcript matches what the reader said.
- After 12s with no words, the dictation says it cannot hear the Omi (or
  the reader) instead of listening in silence.
- Live words survive a segment reset mid-dictation.
- Wear tips only for Omi devices, with the device image and accurate
  button copy ("press to ask, again to send"); "Just talk. Your Omi is
  listening." under the dictation title.
- A firmware update found during onboarding is shown once Home opens,
  not over the first-run screens.
- Connect Omi: sentence-case status ("Tap your Omi to connect.",
  "Connected"), no duplicate "pairing successful", and the "Don't have
  your Omi yet?" links hide while connecting.
- Debug-only OMI_ACTIVATION_DEMO=device to review this path on a
  simulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: the redesign replaces the voice step, so wrapper.dart keeps the
redesign's step navigation; main's voice-step back/resume helpers and their
two wrapper tests are dropped with it. The provider and welcome screen keep
both sides (main's pairing telemetry and welcome analytics).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts were main's Paywall Opened tracking against the pilot's Superwall
openers. Both are kept: openUpgradePaywall now logs main's closed source names
(locked_conversation, chat_quota_exceeded, ...) with the current plan, passes
the source to the plans-sheet fallback, and skips the log when the opener
already sent it. UsagePage keeps main's paywallSource and gains the
upgradePlacement for Superwall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ding

The activation's paywall step registers the placement and waits for it to
close; when the pilot is off or nothing is shown it falls back to the plans
sheet. The pilot keeps its debug-only gate, so release builds are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some iOS builds never make the app inactive under the App Store pop-up
(the simulator never does), so closing it left the rating screen waiting
for Continue. The pop-up takes every touch while it is up, so the first
tap on the screen after asking means it closed: go to the paywall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The onboarding activation no longer records a voice profile, so "Teach
Omi your voice" is the reader's prompt to do it. It now has a close
button; closing it hides it for good and a toast points to Settings >
Voice Profile. It still hides once a profile exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut, bounded paywall wait

- Skip (or leaving) while the activation was still starting left the phone
  microphone recording into Home: start() now checks a cancelled flag after
  each wait and stops a microphone that came up after cancel().
- The activation step allowed the iOS edge swipe / Android back to pop the
  whole onboarding back to the welcome screen; it no longer can.
- The Superwall onboarding paywall wait gives up after 5 minutes if the SDK
  never reports a close, so onboarding cannot hang on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The top bar (device, search, settings) is the same on Home and Tasks.
  Connect keeps its word on every page; the logo alone read as a spinner.
- Each page has one bare action beside the Home / Tasks switcher: the
  calendar on Home, a "more" menu on Tasks (select, group, show completed,
  export). This replaces the second row of circles on Tasks and the
  separate "more" button beside the task search.
- Switching pages fades and slides the incoming page in from its tab's
  side (off under Reduce Motion); both pages keep their state.
- Empty Tasks says what will show up there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ask Omi is a round button at the left (tap to type, hold to talk); the
  main control is a light pill: "+ New Recording", "Stop" while the phone
  records (a spinner while it finishes), or the capturing pendant with what
  it is doing (tap for the transcript, hold to record with the phone; an
  Omi call opens the call).
- The live transcript grows out of the pill and shrinks back into it; the
  iOS edge swipe still dismisses it. Reduce Motion fades instead.
- Home's live card steps aside while the pendant is simply listening (the
  pill says it) and stays for every other state.
- Fixes found in review: Stop on a reconnecting phone recording finishes it
  instead of starting another; a connected pendant that is not capturing
  keeps "+ New Recording" so the phone is one tap away.
- Visual-audit frame and the performance test follow the new controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @YinkaAdepoju — this is a substantial, well-structured draft. The fail-closed experiment handling in app/lib/services/experiments/onboarding_activation_flow.dart (control on no-identity/timeout/error, per-account arm memory, debug override that can't reach store builds), the flag registration in config/feature-flags.yaml + backend/docs/feature-flag-registry.md with prereg and review date, the plans-sheet fallback in app/lib/pages/settings/widgets/upgrade_paywall.dart, and the guarded pilot in app/lib/services/superwall_pilot.dart (debug-only, serialized identity sync, 5-minute paywall timeout so onboarding can't hang) all look carefully built. The new app/docs/ux-contract.md permissions section matches permissions_widget.dart (answered-vs-granted semantics, orange/red states, onboarding/bluetoothAskedAtLaunch), and app/e2e/SKILL.md's new step map tracks the file changes.

Dependency note: superwallkit_flutter 2.6.0 is hosted on pub.dev with a pinned sha; Podfile.lock adds SuperwallKit 4.17.0 transitively. The embedded key is a public SDK key and the docs correctly forbid secrets — no integrity concern found there.

Leaving for human maintainer review — these are product decisions rather than code defects:

  1. Monetization direction: the activation arm shows a paywall before Home for every new user. That's a strategy call the maintainers should make explicitly, not something review can settle.
  2. Entry-point ownership vs main: feat(mobile): define storefront presentation contract and paywall design #21354 (contextual upsells) and feat(billing): plans sheet schema_version 2 #21397 (plans sheet v2) overlap the eight Superwall placements here. The PR's own "Risks / open" flags this — a ruling on which stack owns each upgrade entry point would unblock it.
  3. New payments SDK: Superwall sign-off is needed even though it's debug-gated today (configure() requires kDebugMode && iOS && SUPERWALL_TEST_MODE).
  4. UID sharing: SuperwallPilot.syncUser sends the Firebase UID to a third-party service — fine for a debug pilot, but worth an explicit OK for whenever the pilot widens.
  5. Two acknowledged trade-offs: the in-onboarding rating ask (App Store 5.6.3) and the removal of the attribution survey.

Nothing verified as broken, so no change requests from me — the open-questions section is honest and the work is cohesive as one unit.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added needs-maintainer-review Needs a human maintainer to sign off before merge dependency-review Touches dependencies or lockfiles; needs dependency review feature-fit-review Needs review of product/feature direction with Omi mission/vision flutter flutter work labels Oct 14, 2026
Yinka and others added 5 commits October 14, 2026 09:48
…eriment (BasedHardware#21450)

Brings in Nik's paywall A/B test (PaywallRouter, flag paywall-variant) and flag-gated web
onboarding. Resolution:
- openUpgradePaywall: Superwall pilot (when on) -> PaywallRouter arm -> plans sheet; logs
  Paywall Opened once; forceNative carries the web variant's fallback so it never loops.
- Onboarding: the activation ending's paywall asks the router before the onboarding sheet;
  the experiment's end-of-onboarding placement is skipped when the ending already offered a
  plan, so nobody sees two paywalls in a row. Keeps the debug onboarding rerun and the web
  onboarding entry; the first-capture step telemetry stays removed (BasedHardware#21396 revert).
- l10n: main's keys kept; the reverted first-capture strings dropped again; regenerated.
- Flag registry re-rendered; onboarding-activation-flow notes name its PostHog flag/experiment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l experiment

Debug builds no longer configure Superwall unless --dart-define=SUPERWALL_TEST_MODE=true, so
every build exercises PaywallRouter by default. Adds widget tests for openUpgradePaywall:
control -> plans sheet, a variant arm -> its page, forceNative -> sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wall-reask)

Free readers keep opening the app but rarely meet a paywall, since every paywall waits for a
limit. On launch or resume, a free reader whose re-ask is due gets the usual paywall
(openUpgradePaywall -> PaywallRouter), queued in PromptQueue so it never covers a recording or
another prompt. Due 2, 2, 3, 4, then every 7 calendar days after the last paywall from any
source; the first Home visit only starts the clock. Both arms are exposed at the same due
moments; control shows nothing. New Paywall Opened source app_open_reask. Pre-registration:
app/lib/services/experiments/paywall_reask.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, control) and the app-open re-ask

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Simulated on Sep 1-11 device buyers who didn't subscribe: median 2 re-asks in their first
30 days (was 3), at most 5 (was 6); the first ask stays on day 2, when most buyers subscribe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@YinkaAdepoju YinkaAdepoju changed the title feat(app): onboarding that ends in a first conversation (A/B), Home live pill, iOS Superwall pilot feat(app): onboarding that ends in a first conversation (A/B), paywall re-ask on app open (A/B), Home live pill Oct 14, 2026
Yinka and others added 12 commits October 14, 2026 12:28
…p; rating moves on by itself

The App Store rating pop-up gives no close callback and on device the app may never go
inactive under it, so the rating screen stayed up after the pop-up closed. Once the pop-up has
been asked for, the flow now continues to the paywall underneath it. Every onboarding step's
primary button uses one shared bottom gap (kOnboardingButtonBottomGap).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tle; no 'still writing your notes' line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Home fades in; Omi pull-to-refresh

- Dictation: the script lights up word by word with the voice (tick per word, waits on
  silence); Omi's sentences confirm it, or the reader's own words ink in when they said
  something else. A live waveform shows Omi is listening.
- Summarizing: the words stay in the same card and condense (filler folds away, key words
  draw together, the card shrinks); a three-line mark condenses in place of the sparkle. The
  dictation subtitle keeps its space so the card does not move.
- Summary: rises out of a blur, then a light passes across once.
- Leaving onboarding replaces the stack with a fade (omiRootRoute): a slide showed Home moving
  over a root already redrawn as Home (double Home).
- Home and Tasks: iOS pull-to-refresh draws a ring, ticks when armed, then spins
  (OmiRefreshControl); Android keeps the Material indicator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…once, no flash of the old card

While the microphone came up (initialising) the old Listening card showed at the top and the
pill still read New Recording. The pill now turns live the moment it is tapped, and the card
stays hidden for a starting phone recording as it already was for a running one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nside the card

Real notes arrive as markdown (## headings, - bullets, **bold**) and were shown raw; a long
summary also outgrew the card, clipping it and pushing it into the title. SummaryText shows
headings as bold lines and items as bullets, and caps the summary at 190pt: longer notes
scroll inside it behind a bottom fade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Words lit by the voice now stay grey until Omi actually hears them; then each turns full ink
with a soft highlight behind it and a small pop, in turn, with a light tick per sentence. The
reader's own words (when they said something else) flash in the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the row away

The open view drops completed tasks and the completed view drops reopened ones, so rows used to
vanish on tap. The row now shows done/undone at once, holds for a beat, folds closed, and only
then is the task moved. Fold state is keyed per task so the next row never grows back in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the grey skeleton (blank circle, Processing pill, empty bar) with the saved
row's layout: a glyph in the emoji's square whose three lines condense into one,
Processing as the title, time and length, and the first words said with a light
passing over them. Motion runs ~30 s then rests; reduced motion keeps it still.
The timeout state keeps its copy and Try again, now an OmiButton.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al, Continue at thumb height

- Connect Omi: when the Omi connects, a ring draws around it, a check lands on the ring, one
  pulse goes out and the title reads Connected (device name below) before the step moves on
  (~1.8 s; reduced motion holds it still). Previously the tutorial opened the same instant.
- Device tutorial intro: drop the Skip under Get Started; the close X is the way out.
- Tutorial Continue/Finish: the shared OmiButton, ending kOnboardingButtonBottomGap above the
  safe area like every other onboarding step (was a 56pt pill 24pt from the bottom).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Variant arms saw a paywall at the end of first-run onboarding and were
exposed there; control returned early and was only exposed later, if ever.
The arms entered the experiment at different moments, which skews
per-exposed conversion against the variants.

Control now reads its payload too, and when `onboarding_paywall` is on it
is exposed at that moment without being shown anything
(PaywallRouter.exposeControl, same paid / hidden-UI gates as openVariant).
The pre-registration and flag registry note that onboarding_paywall must
match on every arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a no-speech reply told the tutorial the question had failed. A transcription error, quota
error, dropped connection or a stream that ended without an answer left it on 'processing your
question' for good. sendVoiceMessageStreamToServer now takes onFailed for all of those; the
capture controller plays the failure haptic and moves the tutorial to its try-again state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OmiEmptyState's glyph is optional; Home and Tasks drop the forum/check icons above
'No conversations yet' / 'No tasks yet'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kodjima33 added a commit that referenced this pull request Oct 14, 2026
…a three-way onboarding experiment (#21650)

## Summary

Adds Yinka's onboarding (PR #21445: Name → Permissions → Language →
Connect Omi → a first real conversation, its summary and to-dos, the
paywall, the rating ask) as the `yinka` arm of a three-way first-run
onboarding experiment, next to today's native steps (`control`) and
Reid's web page (`reid`). Nothing changes for anyone not in the
experiment: the flag targets the test account only, and an unread flag,
an unknown variant or a bad payload is control.

- `OnboardingVariantGate` replaces `OnboardingWebviewGate`: one
multivariate PostHog flag `onboarding-variant` (`control` / `reid` with
`{"url"}` payload / `yinka`), same identity wait, warm-up,
account-switch fence and verified reload. Debug:
`--dart-define=OMI_ONBOARDING_VARIANT=reid|yinka`.
- Yinka's screens live under `app/lib/pages/onboarding/variants/yinka/`
(copied from her branch, imports rebased) with a small
`YinkaOnboardingFlow` page that runs them after sign-in and consent,
like the web onboarding page. The wrapper still owns completion; it
skips the end-of-onboarding paywall placement when the activation
already offered a plan.
- Shared, additive changes her flow needs: capture controller audio taps
(+ a flag to hold the generic "talk for 30 seconds" notice), onboarding
provider quiet scan / `endSearch`, Bluetooth in the permissions panel
enum, scanning ripple colour, in-app web page close button, the Omi shop
helper, 32 strings in all 49 locales.
- Out of scope on purpose (not onboarding): her Home live pill, Settings
upgrade button, paywall re-ask, Superwall pilot, pull-to-refresh,
Get-Started removal.
- The wrapper emits `Onboarding Variant Shown` with
`$feature/onboarding-variant` for every arm, the experiment's exposure
event.

PostHog: flag 961069 (test account → `yinka`, everyone else control),
draft experiment 484222, dashboard 2212642.

## Testing

Simulator (dev flavour, prod backend, `OMI_ONBOARDING_VARIANT=yinka`):
Name → Permissions → Language → Connect Omi ("Not ordered yet" opens the
store sheet) → "Let's try Omi" → dictation → "You're all set, Nik" →
rating prompt → outline paywall (the account's paywall arm) → Home, no
second paywall. Kill-and-relaunch mid-flow resumed at Connect Omi. 60
unit/widget tests pass (gate, activation flow, connect step, summary
text, existing onboarding tests).

## Product invariants affected

- INV-CAP-1 — the capture controller change only adds observer taps (a
copy of each phone-mic buffer / pendant packet handed to a listener) and
a flag that hides a notice; no uplink, subscription, WAL or frame path
is closed, fenced or skipped.

## Failure class (fixes)

Failure-Class: none

UI-Evidence: none -- new screens are flag-gated to the test account;
simulator walkthrough screenshots in the PR description are the
evidence.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/BasedHardware/omi/pull/21650?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="View guided diff"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<a
href="https://www.cubic.dev/action/auto-fix/pr/BasedHardware/omi/21650?returnTo=https%3A%2F%2Fgithub.com%2FBasedHardware%2Fomi%2Fpull%2F21650&source=description"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"><img
alt="Turn on auto-fix"
src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Reviewed statically (draft, suspicious tier — no code executed). This is a big but coherent change — one product outcome (first-run ending in a first conversation, paywall experiment, Home live pill) — so I'm not asking for a split; feedback below is on substance.

What looks good, with specifics:

  • app/lib/services/experiments/paywall_reask.dart: clean fail-closed design — no identity, unread flag, or any error returns false and exposes nobody; the first Home visit only starts the clock (never asks on onboarding day); control advances the same schedule so both arms are counted at the same moments. Schedule math (gapAfter/daysBetween/isDue) is straightforward and unit-tested in app/test/unit/paywall_reask_test.dart.
  • app/lib/pages/settings/widgets/upgrade_paywall.dart: one entry point for every upgrade surface with ordered fallbacks (Superwall pilot → PaywallRouter variant → existing plans sheet) and a forceNative escape so the web variant's dead page can't loop back into itself.
  • app/lib/pages/paywall/paywall_router.dart: PaywallReask.noteShown() on any router-shown paywall restarts the re-ask clock from any source; exposeControl() gives control-arm readers their exposure event at the same moment variants see a paywall — good experiment hygiene.
  • app/lib/services/superwall_pilot.dart: configure() is inert unless kDebugMode + SUPERWALL_TEST_MODE + iOS; identity updates are serialized through a queue so an earlier identify can't finish last; showPlacementAndWait has a 5-minute timeout so an unreported close can't hang onboarding.
  • app/lib/pages/onboarding/activation/activation_capture.dart: real capture pipeline reuse (device stream or phone mic), cancel-checked after each await so a Skip can't leave the mic recording behind the next screen, and voice-profile enrollment gated on minimum words/seconds so silence doesn't enroll a bad voiceprint.
  • app/integration_test/app_performance_test.dart: find.text('Ask Omi') finders replaced with ValueKey('home_ask_omi_button') — more robust against localization.
  • The l10n sweep across all .arb locales is consistent with the new onboarding strings, and a changelog fragment is present (app/changelog/unreleased/20261013-onboarding-redesign-first-conversation.json).

Things that need a maintainer decision before this leaves draft:

  1. New third-party payment SDK. app/pubspec.yaml adds superwallkit_flutter ^2.6.0 direct (pub.dev, lockfile sha 22c9be1…), pulling native SuperwallKit 4.17.0 + Superscript into app/ios/Podfile.lock. Even though the pilot is iOS-debug-only, the native binary enters every iOS build. That plus item 2 is why dependency-review should stay on.
  2. Default key and uid sharing. superwall_pilot.dart ships a source default public SDK key (pk_EA03…) pointing at a personal Superwall test project, and syncUser() identifies Firebase uids to it. Public SDK keys are designed to ship, but making a personal test project the in-repo default — and sending user identifiers to it, even debug-only — is a privacy/ownership call for a maintainer. If this lands, consider making the key dart-define-only with no default.
  3. Product direction. The PR honestly flags these and they're real decisions: an App Store rating ask inside onboarding (Guideline 5.6.3 exposure), dropping the attribution survey, and a scheduled paywall re-ask for free users (2/3/5/7-day cadence). All are behind not-yet-created PostHog flags and fail closed, so nothing ships by accident — but whether Omi wants this monetization posture is a human call.
  4. CI. No check runs are reported on this head, so the body's local "4,425 passed" claim is unverified here. Please make sure the PR is ready for CI (or rebase so checks trigger) before marking ready for review.
  5. Overlap with fix(onboarding): Watch instructions layout, specific pairing errors, find-us fits on screen #21426: its screens are removed here; the pairing-error improvements it carries would be lost. Worth an explicit ruling on that PR (supersede/extract) so it doesn't linger.

Agent-guidance docs note: this PR rewrites app/e2e/SKILL.md's onboarding tree, adds app/docs/superwall-pilot.md, and extends app/docs/ux-contract.md — all docs that AI coding/review agents in this repo follow. I checked them against the diff and they match the new code (step order, flag names, key-handling rules); the Superwall doc's "never commit a private key" guidance is accurate and safe. Just be aware these now describe a flow that differs from main until this merges, and superwall-pilot.md documents a billing pilot whose adoption is still open.

Thanks @YinkaAdepoju — the engineering discipline here is genuinely good (fail-closed flags, both-arm exposure, fallback chains, tests per new service). Leaving for human maintainer review: the new payment SDK dependency + uid-sharing default need maintainer sign-off, and the onboarding/paywall product direction (rating placement, re-ask cadence, #21426 supersession) is a product decision, not a code-correctness one.

--
Automated maintainer feedback on behalf of the Omi maintainers. AI-generated review; human maintainers make the final call.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@YinkaAdepoju

Copy link
Copy Markdown
Author

Closing in favour of two smaller pieces: the onboarding itself landed as the yinka arm of the onboarding experiment in #21650 (thanks @kodjima33), and the rest (Home redesign, Tasks, sign-in, upgrade entry points) is rebased on main in draft #21711. The Superwall pilot is dropped in favour of #21450's paywall router.

@github-actions

Copy link
Copy Markdown
Contributor

Hey @YinkaAdepoju 👋

Thank you so much for taking the time to contribute to Omi! We truly appreciate you putting in the effort to submit this pull request.

After careful review, we've decided not to merge this particular PR. Please don't take this personally — we genuinely try to merge as many contributions as possible, but sometimes we have to make tough calls based on:

  • Project standards — Ensuring consistency across the codebase
  • User needs — Making sure changes align with what our users need
  • Code best practices — Maintaining code quality and maintainability
  • Project direction — Keeping aligned with our product principles and locked invariants

Before your next PR, please skim:

  • PRODUCT.md — product north star
  • Product invariants — locked rules (shared chat, memory tiers, agent control plane, integrations, brand)

If this was declined for direction or taste, maintainers should cite an invariant ID or open a proposed one — ask if that citation is missing.

Your contribution is still valuable to us, and we'd love to see you contribute again in the future! If you'd like feedback on how to improve this PR or want to discuss alternative approaches, please don't hesitate to reach out.

Thank you for being part of the Omi community!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependency-review Touches dependencies or lockfiles; needs dependency review feature-fit-review Needs review of product/feature direction with Omi mission/vision flutter flutter work needs-maintainer-review Needs a human maintainer to sign off before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants