* Wire onboarding activation into Account, observers, and shared Inertia data
Add onboarding casts/hasFinishedOnboarding, AccessToken ObservedBy,
Platform::connectableOptions, Post/SocialAccount onboarding broadcast hooks,
and lazy onboardingResidual share + SharedData types.
* Register onboarding routes and post-checkout activation redirects.
Wire billing processing and the sidebar checklist so owners land on
activation after subscribe, with locale sidebar/uk onboarding strings.
* Align MCP grant usability with onboarding activation checks
Unbound MCP tokens fall back to the user's current workspace and require
createPost so viewer/unscoped grants neither unlock the checklist nor
broadcast onboarding status.
* Require bound MCP workspace for onboarding activation.
Drop current-workspace fallback from usable MCP grants so checklist
detection and broadcasts match Passport token scoping; viewers still
cannot unlock the MCP step.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden onboarding review findings and tighten locale strings.
Fix Welcome/Persona/TrackPost suites broken by the activation route reuse
and PostObserver analytics side effects, restore Echo poll fallbacks,
reject unbound MCP grants in tests, and drop unused onboarding.mcp keys.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove unused sidebar and MCP authorization locale keys.
Drop dead sidebar menu/theme strings (including the overwritten
workspace label and api_keys nav entry) and unused MCP authorize
app_title/approving copy across all locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix SetLocale crashing on Passport Symfony OAuth responses.
OAuth errors return a raw Symfony Response without withCookie(); attach
the default locale cookie via headers so authorize no longer 500s.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Prompt OAuth guests to log in before rejecting unknown clients.
MCP Inspector often reuses a stale client_id; validateAuthorizationRequest
was returning invalid_client JSON before the login redirect. Guests now
hit /login first, then client validation runs after authentication.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Render Inertia OAuth authorize errors for browser logins.
After login, Inertia follows the intended authorize URL; raw invalid_client
JSON broke that visit. HTML/Inertia requests now get mcp/AuthorizeError
while API JSON clients still receive the OAuth error payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Detect Inertia OAuth error pages via Request::inertia().
Use the framework helper so post-login authorize failures keep returning
an Inertia page instead of raw OAuth JSON.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify OAuth authorize error page detection to expectsJson.
Drop the X-Inertia header sniff; browser and Inertia visits already do
not expectsJson, while API clients still receive the OAuth JSON payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Share MCP authorize layout and drop the error close button.
Keep authorize and authorize-error on the same centered card shell instead of the auth split layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding activation for reviewability and safety.
Use an exists-based MCP check, keep GETs read-only, move sync into
syncAndNotify, clear MCP skips on connect, restrict complete to owners,
and share Echo/poll via one composable.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Move MCP OAuth authorize UX out of the onboarding PR.
Keep the activation checklist focused; OAuth guest/error-page work now
lives on fix/mcp-oauth-authorize-ux.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix corrupted French MCP locale after OAuth key cleanup.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Restore MCP OAuth authorize UX onto the onboarding branch.
Keep authorize error page, guest login-before-client validation, and
SetLocale Symfony cookie fix in #250.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix OAuth prompt=none redirects and harden onboarding tests.
Keep login_required/consent_required as redirects instead of Inertia,
add regression coverage for owner-only activation, require invite email
confirmation, and align MCP connected apps with the sessions list UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding guards and dedupe viewed analytics.
Introduce isOnboardingOpen / belongsToAccount helpers, collapse
duplicated sync/dispatch paths, and capture onboarding.viewed once
per account.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding event, observers, and status helpers.
Tighten Account onboarding predicates, drop nullable broadcast/dispatch
APIs, and collapse repeated observer/controller guards.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Treat in-app users as always having an account.
Add resolveAccount(), tighten belongsToAccount to string ids, and fold
guest residual handling into ResolveOnboardingStatus.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Rename onboarding residual share test to progress.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding status and rename residual to progress.
Use accountOrFail, extract MCP onboarding scope, auto-leave the ready
screen, and send non-onboarding checkout back to accounts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Extract HasAccount and prefer data_get in onboarding flows.
Move account helpers off User, drop nullable sidebarProgress, and
read OAuth/onboarding payloads with data_get.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding checks and extract HasOnboarding.
Use Eloquent + policies for MCP/backfill paths, and move account
onboarding helpers into a dedicated trait.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add trait tests and tidy onboarding imports.
Cover HasAccount and HasOnboarding under Models/Traits, prefer filled() for checkout session ids, and import Throwable instead of FQCN.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify checkout session_id and OAuth error props.
Read session_id via request->string(), and take OAuth error details from the League exception instead of decoding the response body.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify PostObserver onboarding notify path.
Share one otherPosts check for first-create and last-delete instead of separate callbacks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use post author as onboarding sync actor.
Drop Auth::user() preference in PostObserver; checklist sync attributes to $post->user.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify SocialAccountObserver and OAuth authorize flow.
Share create/delete onboarding notify, drop Auth actor fallback to owner, and inline Passport Inertia error handling.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use lazy Inertia props for onboarding partial reloads.
Drop partial-header branching; wrap page props in closures and always redirect completed/dismissed accounts to the calendar.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Defer sidebar onboarding progress and stamp completion as owner-only.
Skip the MCP checklist work on full Inertia visits via deferred shared props,
early-exit token scans, and keep account completion stamps owner-gated.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify deferred onboarding progress share via canShowProgress.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add User firstName for shared auth and simplify onboarding page.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Move User firstName coverage into UserTest.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use first_name directly without empty-name fallbacks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Resolve onboarding sample prompt on the frontend via i18n.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Stamp onboarding completion via the account owner after teammate unlocks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Count only the account owner MCP grant toward onboarding activation.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix OAuth consent auth-token mismatch for mid-activation owners.
Skip deferred onboardingProgress on Passport authorize so Inertia does not
rotate the session authToken, cover happy and stale-token paths in tests,
and polish MCP setup copy plus sidebar/onboarding layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Keep users on onboarding after activation completes.
Stamp completion and re-render the finished checklist instead of
redirecting to the calendar so owners can review the done state.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Clarify Passport consent-view opt-out and guard app-route deferral.
Rename the authorize-only route check and assert onboardingProgress still
defers on calendar, onboarding, and MCP settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden onboarding completion and MCP consent workspace binding.
Reject OAuth approve without a workspace, retry auto-complete until
stamped, send dismissed complete straight to calendar, and cover the
device consent defer opt-out.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Enable activation checklist for self-hosted installs.
Remove the self-hosted onboarding redirects, keep the SaaS-only dismiss backfill, and cover subscription-less owners plus skip/complete destinations.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add GitHub, Hacker News, and directories referral sources.
Expand the welcome referral step with open-source and directory discovery channels.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Refine welcome referral sources and labels.
Split Instagram/Threads, add Founder, and shorten Google, GitHub, AI, and blog option labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Sort accounts platforms alphabetically and drop connect hover plus.
Reuse connectableOptions for the accounts index and remove the unused plus badge on disconnected cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Centralize PostHog once-capture so disabled installs don't burn dedupe keys.
Move isEnabled + Cache::add into PostHogService::captureOnce and route onboarding viewed/step events through it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding backfill to complete every existing open account.
Drop self-hosted and subscription filters; down clears completed_at again.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop PostHog captureOnce and use plain capture for onboarding.
Remove cache-based event dedupe; callers rely on PostHogService::capture gating.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Rename pre-subscription onboarding funnel to Welcome.
Move the ICP steps to /welcome, drop the social-connect checkout gate, hold unpaid members on a subscription-required screen, and keep legacy /onboarding URLs working until the post-subscription checklist lands.
Closes#237
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop legacy /onboarding ICP URL aliases.
Unfinished users re-enter Welcome via EnsureAccountReady on next login; /onboarding stays free for the post-subscription checklist.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify Welcome PostHog event names.
Use welcome.persona/goals/referral and drop the unused checkout case — begin checkout stays on the frontend as checkout.started / begin_checkout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Slim welcome goal options to match the #204 set.
Drop team_collaboration, automate_api, and track_performance so the goals step stays at nine choices.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Split Welcome AI goals into TryPost AI and MCP assistants.
Rewrite ai_content for in-app generation and add use_mcp so Claude/ChatGPT/Cursor intent is captured separately across locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden Welcome goals gate and drop dead checkout UI.
Treat removed goal values as incomplete so mid-funnel users re-select, remove the unused canCheckout branch, and fix the pt-BR welcome progress label.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add password visibility toggle to the login form.
Match the register eye control so users can reveal their password while signing in.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Point the sidebar community link to Discord.
Replace the X stay-updated entry with Join Discord and the trypost.it/discord invite.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Rename the sidebar Discord link to Discord community.
Softer label that matches the other support nav items.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix broken Turkish Discord community translation.
An unescaped apostrophe left a parse error in lang/tr/sidebar.php.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a single-select referral-source step between the goals and connect
steps of onboarding. The choice is stored on users.referral_source and
mirrored to PostHog, mirroring the existing persona and goals steps.
- ReferralSource enum (12 sources) + nullable users.referral_source column
- referralSource()/storeReferralSource() controller actions with the same
self-hosted, subscribed, persona and goals guards as the sibling steps
- connect() now requires a referral source before rendering
- Single-select ReferralSource.vue page mirroring the goals step
- Localized across all 15 locales
- add an end-to-end walk (account gate -> persona -> goals -> connect ->
Stripe) and a self-hosted bypass test, plus the connect no-workspace and
just-exploring-saved cases
- drop the just_exploring exclusivity: the backend now saves any valid goal
combination (the front-end still clears siblings as a UX nicety), removing
the withValidator rule, the unused Goal::isExclusive(), and the now-unused
goals_exclusive copy
After picking who they are, users now pick what they want to achieve with
TryPost. A multi-select goal step (12 options + an exclusive "just exploring"
and "something else") sits between the persona step and connect, mirroring the
persona screen's style.
The goals persist to a json column on users and are mirrored to PostHog on
identify (onboarding_goals array plus a boolean per goal), so campaigns can be
cross-tabbed against the intent they actually attracted. connect now requires
both a persona and at least one goal; persona store advances to the goal step.
Options are grounded in TryPost's real capabilities (publishing, AI content,
brand voice, automation via API/MCP, collaboration, analytics) and copy is
localized in en/es/pt-BR.
The persona options mix individual roles (creator, developer, marketer) with
organization types (startup, agency, store), but the prompt only asked what
describes 'you'. Make it read intentionally:
- Broaden the prompt to 'you or your business'.
- Creator -> Content creator and Marketer -> a proper role label (es/pt:
Profesional/Profissional de marketing) so the role group is consistent.
Completes the persona grid to a 3x3 with two high-value segments:
Marketer (in-house marketing / social media, a scheduling tool's core user)
and Online store (e-commerce). Labels in all three locales, with
IconSpeakerphone and IconShoppingBag glyphs. Other stays last.
Adds a Developer option to the onboarding persona step (after Freelancer),
with labels in all three locales (Developer / Desarrollador / Desenvolvedor)
and an IconCode glyph in the persona grid. The option flows to the frontend
via Persona::cases() and is accepted by the existing enum validation.
Add a connect step between persona selection and Stripe checkout: persona ->
connect >=1 network (server-enforced) -> checkout. Extract the network grid into
a shared NetworkConnectGrid used by both onboarding and the accounts page, which
is redesigned from a table into the same grid (one account per network, with
per-card connect / connected+disconnect / reconnect states). Drop the openDialog
auto-open flow now that networks are shown inline.
Pricing
- Bill per workspace ($12/mo or $120/yr each); Stripe quantity tracks the
workspace count and syncs on workspace create/delete.
- 2,500 AI credits per workspace, pooled at the account level; monthly reset
on the billing anniversary, annual granted upfront (no rollover).
- One social account per network per workspace; remove all count-based limits
(workspace/social/member) and the legacy plan tiers (single Workspace plan).
Onboarding (cloud only: SELF_HOSTED=false + PostHog)
- Replace the /subscribe plan picker with /onboarding persona selection
(Creator/Freelancer/Startup/Agency/Small business/Other), saved on the user
(users.persona) and mirrored to PostHog, then Stripe Checkout on the monthly
price. 8-day trial so Stripe displays 7.
Billing screen
- Remove the Change Plan dialog (dead with a single plan); add an annual-upgrade
banner for monthly subscribers (swapToYearly).
- Current-plan card shows the workspace count instead of the plan name.
System AI
- Brand analyzer / workspace autofill is always allowed and never debits credits
(system feature, not the user's usage).
Self-hosted (SELF_HOSTED=true) bypasses all billing, credit, limit, network,
and onboarding logic.
The onboarding Brand page was reusing settings.brand.* keys, which
coupled two unrelated pages: a rename in Settings would silently break
onboarding, and the two pages might want to diverge copy later (e.g.
more welcoming language during first-run vs terse settings labels).
Duplicated the relevant keys (field labels, placeholders, tone
options, content language copy) into onboarding.brand.* across
en/pt-BR/es. The Settings workspace page keeps using
settings.brand.* — they're the source of truth for that page now.
No behavior change; just separation of ownership.
Users on the Brand step can type their website URL and click
'Preencher' (Portuguese) / 'Autofill' (English). The backend fetches
their homepage, parses standard meta tags, and returns:
- name ← og:site_name | title (suffix stripped at ' | ', ' - ')
- description ← meta[name=description] | og:description
- language ← <html lang> mapped to en / pt-BR / es
- logo ← apple-touch-icon | largest link[rel*=icon] | og:image
The logo is downloaded, validated (mime whitelist, 2MB max), and
attached to the workspace's 'logo' media collection so the avatar
updates immediately.
Zero LLM calls, zero external APIs. Uses symfony/dom-crawler +
symfony/css-selector (newly required) for meta extraction. Everything
else (Http client, workspace media, Intervention) was already in the
project.
Security:
- SSRF guardrail: resolves the host, rejects private / loopback /
link-local ranges, enforces http(s) scheme on both the initial
page fetch and the logo download.
- Rate limited at 10 req/min per user via the throttle middleware
alias on the route.
- Logo content-type must be one of the allowed image mimes; wrong
types are silently dropped so users never see broken images.
UX:
- 'Autofill' button next to the website input, disabled until there
is a URL; shows a spinner while running.
- If a logo was captured, a small preview appears below the input
so users can see what was pulled before saving.
- Success and error paths both surface as vue-sonner toasts, with
translations in en / pt-BR / es.
- Failures leave the form untouched — nothing is destructively
overwritten if parsing gave us nothing.
Tests (16 new): action-level coverage for happy path, title-suffix
fallback, language code normalization across 6 locales, scheme
rejection, private-range SSRF rejection, implicit https prefixing,
empty sites, upstream errors, and wrong-mime logo rejection. Plus
two controller-level tests for the autofill endpoint.
After users pick their persona (role), they now land on a new Brand
step that collects the same fields available in Settings → Workspace
→ Brand: website, description, tone, voice notes, and content
language. When they continue, every AI-generated post for this
workspace already has sensible defaults — before the user's first
post is ever drafted.
Flow:
Role (persona) → Brand (new) → Connections → Subscription → Completed.
A 'Skip for now' button on the brand step advances to Connections
without touching the workspace (defaults stay at their seed values).
Backend:
- Setup enum gets a new Brand case slotted between Role and
Connections with matching stepNumber updates.
- OnboardingController::brand() renders the form pre-filled from the
current workspace. storeBrand() validates via a new
StoreBrandRequest form request and writes the fields onto the
workspace, then advances setup. skipBrand() just advances.
- storeRole() redirects to brand instead of account. enforceStep()
knows how to redirect users whose setup is Brand.
- Three new routes: GET /onboarding/brand, POST /onboarding/brand,
POST /onboarding/brand/skip.
Frontend:
- New Brand.vue page mirrors the Settings brand form but inside the
onboarding AuthLayout. Tone + language sit side by side, both
selects take full width. Translations added to en, pt-BR, and es.
- Wayfinder regenerated so the page can import storeBrand / skipBrand.
Tests:
- Renamed 'redirects to step2' → 'redirects to brand step' and
assert new setup.
- Added six new tests covering brand step auth, redirects, render,
successful store, validation of tone and content_language, and
skip.
- Updated UserSetupTest for the new enum case + reshuffled step
numbers.