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:
Arnaud
2026-07-24 14:04:27 +02:00
parent 1dd8abfd52
commit 1fe379bcdc
15 changed files with 240 additions and 32 deletions
+22 -17
View File
@@ -117,6 +117,7 @@ export default async function RecipePage({ params }: Params) {
mealPairing: !featureFlags.meal_pairing[viewerTier],
nutritionEstimation: !featureFlags.nutrition_estimation[viewerTier],
markdownExport: !featureFlags.markdown_export[viewerTier],
versionHistory: !featureFlags.version_history[viewerTier],
};
// A feature disabled for every tier has no upgrade path, so it hides
// outright; one enabled for at least one tier still shows (locked, with a
@@ -127,6 +128,7 @@ export default async function RecipePage({ params }: Params) {
mealPairing: isFeatureAvailableAnyTier(featureFlags, "meal_pairing"),
nutritionEstimation: isFeatureAvailableAnyTier(featureFlags, "nutrition_estimation"),
markdownExport: isFeatureAvailableAnyTier(featureFlags, "markdown_export"),
versionHistory: isFeatureAvailableAnyTier(featureFlags, "version_history"),
};
const isOwner = recipe.authorId === session.user.id;
@@ -289,23 +291,26 @@ export default async function RecipePage({ params }: Params) {
)}
{isOwner && (
<>
<VersionHistoryButton
recipeId={id}
currentSnapshot={{
title: recipe.title,
description: recipe.description,
ingredients: recipe.ingredients.map((ing) => ({
rawName: ing.rawName,
quantity: ing.quantity,
unit: ing.unit,
note: ing.note,
})),
steps: recipe.steps.map((s) => ({
instruction: s.instruction,
timerSeconds: s.timerSeconds,
})),
}}
/>
{available.versionHistory && (
<VersionHistoryButton
recipeId={id}
currentSnapshot={{
title: recipe.title,
description: recipe.description,
ingredients: recipe.ingredients.map((ing) => ({
rawName: ing.rawName,
quantity: ing.quantity,
unit: ing.unit,
note: ing.note,
})),
steps: recipe.steps.map((s) => ({
instruction: s.instruction,
timerSeconds: s.timerSeconds,
})),
}}
locked={locked.versionHistory}
/>
)}
<Tooltip>
<TooltipTrigger render={
<Link href={`/recipes/${id}/edit`} className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}>