feat: version history as 10th feature flag, richer upgrade prompts, close unguarded photo-import tab (v0.80.0)
Recipe version history is now gated like the other 9 per-tier features (enabled by default, locked-vs-hidden treatment via isFeatureAvailableAnyTier). UpgradeDialog now shows a tagline + concrete bullet list per feature instead of one generic sentence. Also closes a real gap: the AI-generate dialog's Photo tab hit /api/v1/ai/import-photo directly with no tier check in the UI (server route was already correctly gated) — a locked-out user could fill it in and hit a dead-end error instead of an upgrade prompt. Now threaded through the same lock/hide props as the dedicated PhotoImportButton. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -116,8 +116,9 @@ Status legend: **Exists** (fully working) · **Partial** (works but with a real
|
||||
| Stripe webhook (checkout/cancel) | Exists | Signature-verified, replay-protected, event-deduped — production quality | `apps/web/app/api/webhooks/stripe/route.ts` |
|
||||
| Stripe checkout + self-serve billing portal (2026-07-23) | Exists | `POST /api/v1/billing/checkout` creates a subscription Checkout Session (promotion codes enabled); `POST /api/v1/billing/portal` opens Stripe's hosted Customer Portal for self-serve cancel/upgrade/card-update. Webhook route rewritten with the real `stripe` SDK (`stripe.webhooks.constructEvent`, replacing the hand-rolled HMAC verifier) and now handles the full event set: `checkout.session.completed`, `customer.subscription.{updated,deleted}`, `invoice.{payment_failed,paid}` — `past_due` deliberately doesn't downgrade tier (Stripe retries the card first). `/settings/billing` shows plan cards, usage, and a "Manage billing" button; `/admin/billing` shows connection status, subscriber counts, past-due list, recent billing audit events. Cancel/downgrade is at period end (Stripe Portal default); no trial period. **Not yet built:** family-group multi-user sharing (Family tier is purchasable solo, but the plan's per-account member invite/join/tier-resolution piece is deliberately deferred — see `plans/STRIPE_PLAN.md` §1a, flagged there as the most novel/error-prone piece, intentionally shipped after solo billing is proven). | `apps/web/lib/stripe.ts`, `apps/web/app/api/webhooks/stripe/route.ts`, `apps/web/app/api/v1/billing/**`, `apps/web/app/(app)/settings/billing/page.tsx`, `apps/web/app/admin/billing/page.tsx` |
|
||||
| Admin dashboard | Exists | 15 sections (was 13, missing Billing until this pass): overview, insights/analytics, users, invites, recipe moderation, reports, support, tier limits, **billing**, webhooks, audit logs, storage, AI config, site settings, changelog | `apps/web/app/admin/layout.tsx` (`adminNav`) |
|
||||
| Feature flags (per-tier) vs feature prefs (per-user cosmetic) | Exists, two distinct systems | Flags gate 9 capabilities by tier: recipe_variations/drink_pairing/meal_pairing (default enabled) plus recipe_import_url, recipe_import_photo, nutrition_estimation, markdown_export, weekly_nutrition, grocery_delivery (default **disabled** for all tiers — admin turns on per tier from `/admin/tiers`). Each key's own `defaultEnabled` governs the fallback when no admin override row exists, not a blanket true. Prefs let users hide 7 nav sections, no billing implication, unrelated system. | `apps/web/lib/{feature-flags,feature-prefs}.ts` |
|
||||
| Locked-vs-hidden rule for gated features (standardized 2026-07-24) | Exists | Consistent rule across all 9 flags via `isFeatureAvailableAnyTier()`: if a feature is enabled on **at least one** tier, it stays visible for locked-out viewers with a small "Pro" badge (in the tooltip for icon buttons, inline for text buttons) and clicking opens `UpgradeDialog` instead of the real action; if a feature is disabled on **every** tier, it hides entirely (no dead-end upsell for something nobody can unlock). Previously inconsistent — some features hid outright, one (variations) showed a lock icon overlapping its own icon. Applies to: variations, meal/drink pairing, nutrition estimation, markdown export (5 call sites: recipe/meal-plan/shopping-list/collection/pantry), weekly nutrition, import-from-URL, import-from-photo, and the Instacart grocery-delivery menu item (which additionally requires `NEXT_PUBLIC_GROCERY_PROVIDER=instacart` to be configured before it's ever considered "available"). | `apps/web/lib/feature-flags.ts` (`isFeatureAvailableAnyTier`), `apps/web/components/premium/pro-badge.tsx` |
|
||||
| Feature flags (per-tier) vs feature prefs (per-user cosmetic) | Exists, two distinct systems | Flags gate 10 capabilities by tier: recipe_variations/drink_pairing/meal_pairing/version_history (default enabled) plus recipe_import_url, recipe_import_photo, nutrition_estimation, markdown_export, weekly_nutrition, grocery_delivery (default **disabled** for all tiers — admin turns on per tier from `/admin/tiers`). Each key's own `defaultEnabled` governs the fallback when no admin override row exists, not a blanket true. Prefs let users hide 7 nav sections, no billing implication, unrelated system. | `apps/web/lib/{feature-flags,feature-prefs}.ts` |
|
||||
| Locked-vs-hidden rule for gated features (standardized 2026-07-24) | Exists | Consistent rule across all 10 flags via `isFeatureAvailableAnyTier()`: if a feature is enabled on **at least one** tier, it stays visible for locked-out viewers with a small "Pro" badge (in the tooltip for icon buttons, inline for text buttons) and clicking opens `UpgradeDialog` instead of the real action; if a feature is disabled on **every** tier, it hides entirely (no dead-end upsell for something nobody can unlock). Previously inconsistent — some features hid outright, one (variations) showed a lock icon overlapping its own icon. Applies to: variations, meal/drink pairing, nutrition estimation, markdown export (5 call sites: recipe/meal-plan/shopping-list/collection/pantry), weekly nutrition, import-from-URL, import-from-photo, recipe version history, and the Instacart grocery-delivery menu item (which additionally requires `NEXT_PUBLIC_GROCERY_PROVIDER=instacart` to be configured before it's ever considered "available"). `UpgradeDialog` itself now shows a per-feature tagline + bullet list (`FEATURE_COPY` in the dialog component) instead of one generic sentence for every feature. | `apps/web/lib/feature-flags.ts` (`isFeatureAvailableAnyTier`), `apps/web/components/premium/{pro-badge,upgrade-dialog}.tsx` |
|
||||
| AI-generate dialog's Photo tab bypassed the photo-import gate (closed 2026-07-24) | Exists | A second, unguarded front door into `recipe_import_photo` — the tab in `AiGenerateDialog` had no tier check at all, while the dedicated `PhotoImportButton` on `/recipes/new` did. Server-side `requireFeatureEnabledResponse` on the API route meant a fully-disabled feature still 403'd, but a viewer without the tier could reach the tab, fill it in, and hit a dead-end error instead of an upgrade prompt. Now the tab hides/locks in step with `PhotoImportButton`, threaded from `RecipesHeader`. | `apps/web/components/recipe/ai-generate-dialog.tsx`, `apps/web/components/recipe/recipes-header.tsx` |
|
||||
| **Developer permission** (2026-07-22, split 2026-07-23) | Exists | Two separate, orthogonal permissions — not one. `users.isDeveloper` gates webhooks + self-serve API keys: admin-toggled always, *and* self-serve toggleable by the user themselves once on a paid tier (no added fee) via `PATCH /api/v1/users/me/developer-access`; free-tier users still need an admin grant. `users.isByokEnabled` gates BYOK separately, admin-only, no self-serve — routing real AI spend through Epicure on the user's own key warrants a manual check-in. Previously all three (webhooks/API keys/BYOK) shared one flag with zero self-serve path. Existing users with a webhook/API key were already grandfathered for `isDeveloper`; a second migration grandfathered existing BYOK users into `isByokEnabled` specifically. | `apps/web/lib/permissions.ts` (`hasDeveloperAccess`, `hasByokAccess`, `canSelfServeDeveloperAccess`), `apps/web/lib/api-auth.ts` (`requireDeveloper`, `requireByok`) |
|
||||
| User webhooks (personal automation) | Exists | 7 events, Zapier-style, requires developer access | `apps/web/lib/webhooks.ts` |
|
||||
| Admin ops webhooks (site-wide) | Exists | 3 events (signup, ticket, report) — admin-only, unrelated to developer access | `apps/web/lib/admin-webhooks.ts` |
|
||||
|
||||
Reference in New Issue
Block a user