Skip to content

Free First Practice — Promo Replacement + Lead Funnel Test

Replace the 50-free-credits trial promo with the existing CHERRY_CITY_FIRST_PRACTICE comp offer across all promo surfaces, and test the free practice as the primary lead-capture CTA on the Cherry City market LP in place of the hero tour form.

The bet

The tour form produces almost no scheduled tours — tour scheduling happens through DM conversations. Meanwhile the 50-credit promo asks prospects to first understand prepaid credits before they can see the value, which is backwards for a first touch. A comped first practice is a concrete, self-explanatory offer, and since a CM is required to show up for every first practice, it is effectively a tour — with a practice session instead of a walkthrough. The hypothesis: on-site free-practice bookings will convert at a higher rate than on-site tour-form submissions (a low bar — near zero) while getting the same or better in-person CM contact.

Scope: Cherry City (MG10) only — the only location with hourly enabled. The comp primitive is location-scoped per offer, so expansion later is a data change.

What already exists (no work)

  • Comp primitive (comp-reservations spec): public POST /api/comp/reservations, Offer-row-driven, stub users by email, no card, PromoRedemption dedup, account-setup email chain, comp_booked PostHog event, Meta CAPI + Google Lead events.
  • CHERRY_CITY_FIRST_PRACTICE offer: FREE_BOOKING, 1/user, 240-min cap, MG10 hourly studios, no end date.
  • Comp booking flow: /book?type=hourly&offer=<code> (BookingFlow comp mode with the native studio picker, per the scheduler refactor). The pre-refactor HourlyBookingWithPicker comp modal is deleted.
  • Hourly staff-notification email fires for comp bookings via executeCreationSideEffects — CM hears about every booking.
  • AlreadyRedeemedModal → falls through to paid hourly booking.
  • Minimum booking lead time is governed by Acuity availability — the scheduler only presents valid slots, so no CM-scramble gate is needed on the comp endpoint.

Workstream A — swap promo surfaces (50 credits → free first practice)

A1. Dashboard callout (/account/overview) — data only, zero code

GET /api/offers/dashboard returns Offer rows with showOnDashboard: true; FREE_BOOKING passes the type filter by default and is hidden once the user has a PromoRedemption row. handleOfferPress in UnifiedDashboardContent.tsx falls through to router.push(offer.ctaUrl) for unknown types.

Script (apps/api/scripts/marketing/, per ops-script convention):

  • 50-credit offer: showOnDashboard: false
  • CHERRY_CITY_FIRST_PRACTICE: showOnDashboard: true + display fields — title, description, ctaText ("Claim your free practice"), ctaUrl: '/lp/salem/free-first-practice', badgeText, priority, variant, imageUrl

A2. Header PromoButton — code change

Current: hardcoded FEATURED_OFFER_ID (CREDIT offer), label {amount} Free Credits, links /promo/free-credits. Eligibility endpoint (/api/promo/eligibility/[offerId]) filters type: 'CREDIT' and requires creditId + amount.

Changes:

  • Eligibility route: accept type IN ('CREDIT', 'FREE_BOOKING'); skip the creditId/amount gate for FREE_BOOKING (amount nullable in response). PromoRedemption check already covers both types.
  • Response: add ctaUrl so the button's destination is offer-driven.
  • PromoButton: label from offer.ctaText (drop the {amount} Free Credits template), link from ctaUrl, swap FEATURED_OFFER_ID to the first-practice offer id.

A3. /promo/free-credits retirement

301 → /lp/salem/free-first-practice in next.config.ts. The CREDIT offer row stays (soft, not deleted) so any already-granted balances and history are untouched; it just loses all surfaces. give-50-get-50 referral program is unrelated — untouched.

Workstream B — LP hero (/lp/salem) — SHELVED pending scheduler refactor

The bet framing stands: "first practice on us" is a higher-value bait than "take a tour". But three hero iterations (inline 2-col scheduler, accordion-only card with studio step, inert scheduler-tease + CTA) all failed the look/feel bar — the accordion scheduler is modal-native and can't carry a marketing hero. Decision (2026-07-09): /lp/salem keeps the tour-form hero unchanged; the free-practice hero lands together with a wholesale scheduler refactor (single-column wizard-style component; SchedulerAccordion backs every booking flow incl. reschedule, so it's a dedicated project — HeroUI OSS has no stepper).

Update (2026-07-12): the scheduler refactor landed as PR #880, and this branch is stacked on it. Comp booking now runs on /book?type=hourly&offer=<code>; the legacy modal stack (HourlyBookingWithPicker, HourlyBookingContent, SchedulerAccordion, CmHelpFooter) is deleted. A future hero integration builds on (or links to) the /book flow, not the retained props below. Open: the modal's "call the CM" help footer has no /book successor yet (see open items).

  • Offer surface meanwhile: the dedicated LP /lp/salem/free-first-practice (content-first hero, "Claim your free practice" CTA navigates to /book?type=hourly&offer=<code>). All legacy slugs (/lp/cherry-city-first-practice, /lp/cherry-city-free-hour, /lp/cherry-city/*) 301 there or to the market LP as appropriate. Promo surfaces (A1/A2/A3) point at it.
  • ~~Retained for the refactor: schedulerOnly (HourlyBookingContent), studioStep (HourlyBookingWithPicker), stepsCollapsed/onStepExpand (SchedulerAccordion chain), SchedulerStepTitle, CmHelpFooter variant="inline" — all tested, opt-in, currently unused by any caller.~~ Deleted 2026-07-12 with their host components; the refactor they waited on shipped a different shape.
  • Config: frontend map marketSlug → compOfferCode in apps/web/src/lib/config/comp-offers.ts (only salem), consumed by the dedicated LP route; the future hero integration reads the same map.
  • Measurement: unchanged plan — comp_booked (offerCode) vs tour_events, once the offer gets a hero-level surface.

Open items

  • Authenticated prefill. Logged-in users clicking the dashboard/header CTA land on the public LP and retype their email. Acceptable v1; prefill from session later if it shows up in friction.
  • Existing members can redeem. Any user without a PromoRedemption row is eligible, including long-tenured members. Accepted — dashboard callout implies it; goodwill cost is one comped session each.
  • CM help contact dropped with the modal. The legacy comp modal's CmHelpFooter (tel: link to the location's community manager) has no successor on /book; useHourlyResource still fetches communityManagerName/communityManagerPhone but nothing renders them. Needs a product call: reintroduce a help affordance on /book or drop the dead fields.