Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
00c48fc
Add post-subscription onboarding design spec.
paulocastellano Jul 24, 2026
5b8e6ff
Add post-subscription onboarding implementation plan.
paulocastellano Jul 24, 2026
44fb9be
Add account onboarding completed and dismissed timestamps.
paulocastellano Jul 24, 2026
a69b799
Add ResolveOnboardingStatus for activation checklist state.
paulocastellano Jul 24, 2026
abf247e
Fix ResolveOnboardingStatus null-account guard and workspace scoping …
paulocastellano Jul 24, 2026
7655c1f
Rename pre-subscription ICP flow to Welcome routes.
paulocastellano Jul 24, 2026
7786204
Fix Task 3 review: rename SocialController welcome test label.
paulocastellano Jul 24, 2026
9827714
Add Welcome Vue pages and i18n; drop social from subscribe.
paulocastellano Jul 24, 2026
1c13007
Add post-subscription onboarding activation endpoints.
paulocastellano Jul 24, 2026
ea615a6
Add single-page post-subscription onboarding UI.
paulocastellano Jul 24, 2026
4726204
Send post-checkout users to onboarding instead of accounts.
paulocastellano Jul 24, 2026
ec64768
Add dismissible residual onboarding banner in app shell.
paulocastellano Jul 24, 2026
ff462e5
Track welcome and onboarding activation events in PostHog.
paulocastellano Jul 24, 2026
254e7b4
Fix post-subscription onboarding review findings.
paulocastellano Jul 24, 2026
fb7a00b
Remove welcome subscribe step and start Stripe from referral.
paulocastellano Jul 24, 2026
5dd4983
Use AppLayout with sidebar on the onboarding activation page.
paulocastellano Jul 24, 2026
8659da8
Polish welcome and onboarding UX and unify the sidebar account menu.
paulocastellano Jul 24, 2026
a1254b4
Refactor billing configuration and subscription checkout process.
paulocastellano Jul 26, 2026
0d75b54
Polish onboarding realtime updates and tighten activation code.
paulocastellano Jul 26, 2026
efc902f
Localize onboarding and welcome strings across all locales.
paulocastellano Jul 26, 2026
996310c
Translate remaining welcome progress labels across locales.
paulocastellano Jul 26, 2026
54b57f2
Add workspace MCP settings page and polish sidebar nav.
paulocastellano Jul 26, 2026
a7f0865
Align MCP settings cards with TryPost neobrutalist UI.
paulocastellano Jul 26, 2026
64354fb
Make ChatGPT MCP icon use currentColor for dark tiles.
paulocastellano Jul 26, 2026
de12c16
Fix MCP accordion cards clipping their bottom border.
paulocastellano Jul 26, 2026
5260f4e
Match Claude and ChatGPT MCP connect cards to onboarding.
paulocastellano Jul 26, 2026
0f19048
Remove open-state border under MCP accordion titles.
paulocastellano Jul 26, 2026
5d39ff0
Give open MCP accordion content a bit more top spacing.
paulocastellano Jul 26, 2026
74d558d
Add MCP link to the workspace section of the sidebar.
paulocastellano Jul 26, 2026
c423fcf
Point MCP settings docs link to the AI introduction page.
paulocastellano Jul 26, 2026
f3df17b
Drop the orphaned step number in other MCP client cards.
paulocastellano Jul 26, 2026
b7d427f
Keep Settings inactive in the sidebar on the MCP page.
paulocastellano Jul 26, 2026
b93153f
Tighten MCP settings copy and match HeadingSmall hierarchy.
paulocastellano Jul 26, 2026
a626edc
Localize MCP settings copy across all supported languages.
paulocastellano Jul 26, 2026
75f5017
Use a white background for the Cursor MCP client tile.
paulocastellano Jul 26, 2026
147b3bf
Harden onboarding MCP detection and post-checkout redirects.
paulocastellano Jul 26, 2026
0af1bae
Align account-scoped MCP onboarding and name the MCP route.
paulocastellano Jul 26, 2026
8455441
Broadcast MCP onboarding account-wide and list team connections.
paulocastellano Jul 26, 2026
5bd5099
Attribute onboarding analytics to the acting user and sync only them.
paulocastellano Jul 26, 2026
57701d5
Harden post-subscription onboarding edge cases from review.
cursoragent Jul 29, 2026
176ee41
Close remaining onboarding review follow-ups.
cursoragent Jul 29, 2026
ca2d350
Harden onboarding against transaction side effects and access gaps.
cursoragent Jul 29, 2026
0a7a590
Align MCP/API workspace access with hasAppAccess.
cursoragent Jul 29, 2026
8a46a5e
Broadcast onboarding complete/dismiss to clear residual immediately.
cursoragent Jul 29, 2026
9db85d8
Clear residual and sync sibling Echo on every onboarding stamp.
cursoragent Jul 29, 2026
6d470bb
Harden onboarding dismiss terminality and avoid double navigation.
cursoragent Jul 29, 2026
90bc604
Redirect completed onboarding revisits and fix residual progress.
cursoragent Jul 29, 2026
b6c72f0
Keep celebration after OAuth stamps completion via session flash.
cursoragent Jul 29, 2026
c833032
Prevent poll/Echo from consuming onboarding celebration flash.
cursoragent Jul 29, 2026
739e198
Polish checkout trial typing and align onboarding account scope.
cursoragent Jul 29, 2026
d36b7c0
Remove dead tryMarkAccountComplete and cover incomplete_expired trial.
cursoragent Jul 29, 2026
2dbabca
Merge origin/main into post-subscription onboarding.
cursoragent Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,17 @@ STRIPE_SECRET=
STRIPE_WEBHOOK_SECRET=
# Set to false to allow generic signup trial without requiring a card.
REQUIRE_CARD_FOR_TRIAL=true
# Free trial length in days, used only for the no-card generic trial above.
# Checkout trial length in days (card collected up front when REQUIRE_CARD_FOR_TRIAL=true).
# - >0: Stripe Checkout trial for N days for first-time signups only (no prior
# real subscription). Values below 2 are clamped to 2 (Stripe's 48h min).
# - 0: no Checkout trial; charge starts immediately.
# - Returning / canceled accounts never get another Checkout trial.
# - When REQUIRE_CARD_FOR_TRIAL=false, Checkout never applies a trial (generic
# signup trial uses this value instead; must be >= 1 in that mode).
# Empty = default 8.
CASHIER_TRIAL_DAYS=8
# Stripe Coupon ID (amount_off, duration=once) so the first invoice at
# checkout comes out to $1 instead of the full monthly price.
STRIPE_FIRST_MONTH_COUPON_ID=
# Show Stripe Checkout promotion-code field.
CASHIER_ALLOW_PROMOTION_CODES=true

# Stripe Plan Price IDs (one per plan × interval). Used by PlanSeeder.
STRIPE_WORKSPACE_MONTHLY=
Expand All @@ -212,6 +218,10 @@ NIGHTWATCH_TOKEN=
# BLUESKY_ENABLED=true
# TELEGRAM_ENABLED=true

# Onboarding MCP client settings URLs (optional overrides)
# MCP_CLAUDE_SETTINGS_URL=https://claude.ai/customize/connectors
# MCP_CHATGPT_SETTINGS_URL=https://chatgpt.com/plugins#settings/Connectors?create-connector=true&redirectAfter=%2Fplugins

# Media Services
UNSPLASH_ACCESS_KEY=
UNSPLASH_SECRET_KEY=
Expand Down
552 changes: 552 additions & 0 deletions .superpowers/plans/2026-07-24-post-subscription-onboarding.md

Large diffs are not rendered by default.

198 changes: 198 additions & 0 deletions .superpowers/specs/2026-07-24-post-subscription-onboarding-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Post-Subscription Onboarding Design

**Date:** 2026-07-24
**Branch:** `feature/post-subscription-onboarding`
**Status:** Approved for planning

## Problem

TryPost’s current “onboarding” is ICP qualification (persona, goals, referral source) plus a mandatory social connect before Stripe. After payment, `/billing/processing` sends the user to `/accounts`. There is no activation moment — no guided path to connect an MCP agent, connect networks in context, or create a first post.

The ICP for TryPost is people who create content via MCP clients (Claude, ChatGPT). Without a dedicated post-subscription activation experience, users miss the AHA moment.

## Goals

1. Rename and clarify **Welcome** (pre-Stripe ICP) vs **Onboarding** (post-Stripe activation).
2. After Stripe, land on a **single-page** activation checklist at `/onboarding`.
3. AHA = MCP connected + first post created (**draft is enough**; publish optional).
4. Flow is **skippable**, with a dismissible residual checklist in the app.
5. Move social connect out of pre-Stripe into post-Stripe onboarding.
6. v1 MCP clients: **Claude** and **ChatGPT** only. First-post step is dual: MCP primary + UI fallback.

## Non-goals (v1)

- Hard-gating the app until onboarding is complete.
- Multi-step wizard for activation (no `/onboarding/step-*`).
- Cursor (or other MCP clients) as first-class cards.
- Requiring publish as the AHA.
- Changing the pre-Stripe ICP questions themselves (only routes/names/structure).

## End-to-end flow

```
/register
→ /register/success
→ /welcome (redirect → /welcome/persona)
→ /welcome/persona
→ /welcome/goals
→ /welcome/referral-source
→ /welcome/subscribe (plan + Stripe CTA; no social required)
→ Stripe Checkout
→ /billing/processing?session_id=…
→ /onboarding (single-page checklist)
→ Skip or complete → /calendar
```

Self-hosted: skip Welcome and activation Onboarding (same policy as today’s onboarding skip → calendar).

## URL map

### Welcome (ICP, pre-subscription)

| Method | Path | Name | Purpose |
|--------|------|------|---------|
| GET | `/welcome` | `app.welcome` | Redirect to `app.welcome.persona` |
| GET/POST | `/welcome/persona` | `app.welcome.persona` / `.store` | Persona |
| GET/POST | `/welcome/goals` | `app.welcome.goals` / `.store` | Goals |
| GET/POST | `/welcome/referral-source` | `app.welcome.referral-source` / `.store` | Referral |
| GET | `/welcome/subscribe` | `app.welcome.subscribe` | Plan summary + subscribe CTA |
| POST | `/welcome/checkout` | `app.welcome.checkout` | Start Stripe Checkout |

Legacy redirects (temporary):

- `/onboarding` → `/welcome/persona` (only while unsubscribed / for old bookmarks; after activation ships, subscribed users hitting old paths should not bounce into Welcome)
- `/onboarding/goals` → `/welcome/goals`
- `/onboarding/referral-source` → `/welcome/referral-source`
- `/onboarding/connect` → `/welcome/subscribe`

`EnsureAccountReady`: unsubscribed users → `app.welcome.persona` (not activation `/onboarding`).

### Onboarding (activation, post-subscription)

| Method | Path | Name | Purpose |
|--------|------|------|---------|
| GET | `/onboarding` | `app.onboarding` | Single-page activation checklist |
| POST | `/onboarding/dismiss` | `app.onboarding.dismiss` | Skip / do later |
| POST | `/onboarding/complete` | `app.onboarding.complete` | Explicit complete when all steps done (optional if auto-complete navigates) |

`/billing/processing` success redirect target changes from `/accounts` to `/onboarding`.

## Code rename (Welcome)

| Today | After |
|-------|-------|
| `OnboardingController` | `WelcomeController` |
| `app/Http/Requests/App/Onboarding/*` | `app/Http/Requests/App/Welcome/*` (`StoreWelcomePersonaRequest`, `StoreWelcomeGoalsRequest`, `StoreWelcomeReferralSourceRequest`) |
| `resources/js/pages/onboarding/{Index,Goals,ReferralSource,Connect}.vue` | `resources/js/pages/welcome/{Persona,Goals,ReferralSource,Subscribe}.vue` |
| `OnboardingLayout.vue` | `WelcomeLayout.vue` |
| `lang/*/onboarding.php` (ICP strings) | `lang/*/welcome.php` |
| `tests/Feature/Onboarding/OnboardingControllerTest.php` | `tests/Feature/Welcome/WelcomeControllerTest.php` |
| `app.onboarding.*` (ICP) | `app.welcome.*` |

`Connect.vue` is removed from Welcome. Social connect lives on the activation page (and remains available at `/accounts`).

Checkout no longer requires an existing social account.

## Onboarding page (single page, not a step form)

One dedicated full-page screen with a **3-item checklist** on the same view:

1. **Connect an agent** — Claude and ChatGPT cards with MCP URL / deep-link / copy instructions (link to docs where needed).
2. **Connect a social network** — reuse existing connect UI patterns (platforms + connected accounts), inlined on this page.
3. **Create your first post** — primary: ready-made MCP prompt (“open Claude/ChatGPT and ask…”); fallback: button to create in TryPost UI. Draft counts.

### Live completion

Steps auto-check from real state (poll or Inertia partial reload):

| Step | Done when |
|------|-----------|
| MCP | User has a non-revoked OAuth access token used for MCP (not a Personal Access Token / API key) |
| Social | Current workspace has ≥ 1 social account |
| First post | Current workspace has ≥ 1 post (any status, any `created_via`) |

Primary CTA: continue into the app when all three are done (or celebrate in place then go to calendar).
Secondary: **Skip / I’ll do this later**.

### Gating policy

- **Not blocking.** After the post-checkout land, users can skip and use the product.
- No middleware that traps subscribed users on `/onboarding` on every request.
- Residual checklist in the app shell until completed or dismissed.

## Persistence

Welcome ICP fields stay on `User`: `persona`, `goals`, `referral_source`.

Activation flags on `Account` (billing/subscription owner — preferred over User so workspace members don’t each own account-level activation):

- `onboarding_completed_at` (nullable timestamp)
- `onboarding_dismissed_at` (nullable timestamp)

Semantics:

- Steps themselves are derived from state, not stored booleans.
- `onboarding_completed_at` set when all three steps are true (server-side on dismiss-complete path or when status is loaded and all true).
- `onboarding_dismissed_at` set on Skip.
- Residual UI shows when account is subscribed and both timestamps are null.
- Dismiss hides residual even if steps remain incomplete.
- Completing all three sets `completed_at` and clears residual.

Existing accounts already subscribed: treat as dismissed or completed (migration strategy: backfill `onboarding_dismissed_at = now()` for accounts that already have an active subscription, so we don’t force legacy users into activation).

## Residual checklist

Dismissible banner or compact checklist in the authenticated app layout (not a modal trap):

- Visible when subscribed + `completed_at` null + `dismissed_at` null.
- Link back to `/onboarding`.
- Dismiss action hits `app.onboarding.dismiss`.

Exact UI placement (sidebar vs top banner) is an implementation detail; prefer the lightest pattern that matches existing app chrome.

## Analytics (PostHog)

Follow existing enum pattern under `App\Enums\PostHog\` (e.g. `WelcomeEvent`, `OnboardingEvent`).

Suggested events:

- Welcome: `welcome.persona_saved`, `welcome.goals_saved`, `welcome.referral_saved`, `welcome.checkout_started`
- Onboarding: `onboarding.viewed`, `onboarding.step_completed` (property `step`: `mcp` | `social` | `first_post`), `onboarding.skipped`, `onboarding.completed`

Step properties when useful: `created_via` (`web` | `mcp`) for first post; MCP client if detectable later (v1 may omit client if unreliable).

## Architecture notes

- **WelcomeController** — ICP + subscribe + checkout only; redirects subscribed users to `/onboarding` or calendar per rules (subscribed + activation incomplete → can visit `/onboarding`; subscribed + complete/dismissed → calendar).
- **OnboardingController** — show checklist props (step statuses, platforms, accounts, MCP server URL, sample prompt), dismiss, complete.
- Prefer a small action/service e.g. `App\Actions\Onboarding\ResolveOnboardingStatus` that returns the three booleans + whether residual should show — shared by page, layout shared props, and tests.
- Frontend: one `resources/js/pages/onboarding/Index.vue`; poll step status while the page is open so MCP/OAuth and MCP-created drafts flip checkmarks without a full navigation.
- Wayfinder regenerate after route/controller rename.
- i18n: all new copy in `lang/*/welcome.php` and `lang/*/onboarding.php` (all locales that already have onboarding strings).

## Testing

- Welcome: renamed feature tests; persona → goals → referral → subscribe; checkout **without** social account succeeds; subscribed users don’t see Welcome ICP.
- Legacy route redirects.
- Billing processing redirects to `/onboarding`.
- Onboarding page renders step statuses from fixtures (MCP token, social, post).
- Dismiss sets `onboarding_dismissed_at`; residual hidden.
- Auto-complete when all three true sets `onboarding_completed_at`.
- `EnsureAccountReady` → `/welcome/persona` when unsubscribed.
- Self-hosted skips both flows.
- Residual shared prop / layout behavior covered at least once.

## Open implementation details (resolved in plan, not product)

- Exact MCP “connected” query against Passport tokens/clients.
- Claude/ChatGPT deep-link vs copy-paste URL UX copy.
- Residual component placement in `AppLayout`.
- Whether `onboarding.complete` is a dedicated POST or automatic on status resolve.

## Success criteria

- New paid users land on `/onboarding` after Stripe, not `/accounts`.
- They can connect Claude or ChatGPT, connect a network, and create a draft (via MCP or UI) on one page.
- They can skip and still use the product, with an optional residual nudge.
- Pre-Stripe flow lives under `/welcome/{step}` with aligned PHP/Vue naming.
- ICP collection no longer requires social connect before payment.
9 changes: 4 additions & 5 deletions app/Actions/Billing/StartSubscriptionCheckout.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
namespace App\Actions\Billing;

use App\Models\Account;
use App\Support\Billing\FirstMonthCheckoutDiscount;
use App\Support\Billing\ConfigureSubscriptionCheckout;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;

class StartSubscriptionCheckout
{
/**
* Create a Stripe Checkout session for the given price and return an Inertia
* redirect to it. Quantity tracks the account's workspace count; the first
* month's coupon is applied when the instance requires a card up front, so
* the first invoice charges $1 instead of running a $0 trial authorization.
* redirect to it. Quantity tracks the account's workspace count. Trial days
* and promotion codes come from Cashier env config.
*/
public function redirect(Account $account, string $priceId, string $cancelUrl): Response
{
Expand All @@ -27,7 +26,7 @@ public function redirect(Account $account, string $priceId, string $cancelUrl):
$subscription = $account->newSubscription(Account::SUBSCRIPTION_NAME, $priceId)
->quantity(max(1, $account->workspaces()->count()));

FirstMonthCheckoutDiscount::apply($subscription, $account);
ConfigureSubscriptionCheckout::apply($subscription, $account);

$session = $subscription->checkout([
'success_url' => route('app.billing.processing').'?session_id={CHECKOUT_SESSION_ID}',
Expand Down
Loading
Loading