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
@@ -21,6 +21,8 @@ import { cn } from "@/lib/utils";
import { FakeProgressBar } from "@/components/ui/fake-progress-bar";
import { useLocale } from "@/lib/i18n/provider";
import { BatchCookFields, type BatchCookFieldsState } from "./batch-cook-fields";
import { UpgradeDialog } from "@/components/premium/upgrade-dialog";
import { ProBadge } from "@/components/premium/pro-badge";
const SURPRISE_PROMPTS = [
"A cozy one-pot dish with whatever is in my pantry on a rainy evening",
@@ -57,9 +59,18 @@ type GeneratedRecipe = {
export function AiGenerateDialog({
open,
onOpenChange,
photoTabAvailable = true,
photoTabLocked = false,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
/** Tier feature flag (recipe_import_photo) disabled for every tier —
* hides the Photo tab entirely, since there's no upgrade path. */
photoTabAvailable?: boolean;
/** Available on some tier but not the viewer's — the tab still shows
* (with a "Pro" upsell); generating opens an upgrade prompt instead of
* calling the import API. */
photoTabLocked?: boolean;
}) {
const t = useTranslations("ai.generate");
const tCommon = useTranslations("common");
@@ -90,6 +101,7 @@ export function AiGenerateDialog({
});
const [busy, setBusy] = useState(false);
const [upgradeOpen, setUpgradeOpen] = useState(false);
function handleClose() {
if (busy) return;
@@ -150,6 +162,10 @@ export function AiGenerateDialog({
async function handlePhotoGenerate() {
if (!photoBase64) return;
if (photoTabLocked) {
setUpgradeOpen(true);
return;
}
setBusy(true);
try {
const res = await fetch("/api/v1/ai/import-photo", {
@@ -206,6 +222,7 @@ export function AiGenerateDialog({
const canSubmit = tab === "describe" ? !!prompt.trim() : tab === "photo" ? !!photoBase64 : batchFields.dinners + batchFields.lunches > 0;
return (
<>
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-xl max-h-[85vh] flex flex-col gap-0 p-0">
<div className="overflow-y-auto p-4 space-y-4 min-h-0">
@@ -223,7 +240,7 @@ export function AiGenerateDialog({
<div className="flex gap-1 p-1 bg-muted rounded-lg">
{([
{ key: "describe", label: t("tabDescribe"), icon: Type },
{ key: "photo", label: t("tabPhoto"), icon: Camera },
...(photoTabAvailable ? [{ key: "photo" as Tab, label: t("tabPhoto"), icon: Camera }] : []),
{ key: "batch", label: tBatch("title"), icon: ChefHat },
] as { key: Tab; label: string; icon: React.ElementType }[]).map(({ key, label, icon: Icon }) => (
<button
@@ -231,7 +248,7 @@ export function AiGenerateDialog({
onClick={() => setTab(key)}
disabled={busy}
className={cn(
"flex-1 flex items-center justify-center gap-2 py-2 px-3 rounded-md text-sm font-medium transition-all",
"flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-md text-sm font-medium transition-all",
tab === key
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground"
@@ -239,6 +256,7 @@ export function AiGenerateDialog({
>
<Icon className="h-4 w-4" />
{label}
{key === "photo" && photoTabLocked && <ProBadge />}
</button>
))}
</div>
@@ -370,5 +388,12 @@ export function AiGenerateDialog({
</div>
</DialogContent>
</Dialog>
<UpgradeDialog
open={upgradeOpen}
onOpenChange={setUpgradeOpen}
featureKey="recipe_import_photo"
featureLabel="Import from photo"
/>
</>
);
}