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:
@@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ export function RecipesHeader({
|
||||
sharedUrl,
|
||||
importUrlAvailable = true,
|
||||
importUrlLocked = false,
|
||||
importPhotoAvailable = true,
|
||||
importPhotoLocked = false,
|
||||
}: {
|
||||
count: number;
|
||||
initialQuery?: string;
|
||||
@@ -114,6 +116,9 @@ export function RecipesHeader({
|
||||
* (with a "Pro" upsell) and opens an upgrade prompt instead of the
|
||||
* import dialog. */
|
||||
importUrlLocked?: boolean;
|
||||
/** Same idea, for the AI-generate dialog's Photo tab (recipe_import_photo). */
|
||||
importPhotoAvailable?: boolean;
|
||||
importPhotoLocked?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -340,7 +345,12 @@ export function RecipesHeader({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AiGenerateDialog open={aiOpen} onOpenChange={setAiOpen} />
|
||||
<AiGenerateDialog
|
||||
open={aiOpen}
|
||||
onOpenChange={setAiOpen}
|
||||
photoTabAvailable={importPhotoAvailable}
|
||||
photoTabLocked={importPhotoLocked}
|
||||
/>
|
||||
<UrlImportDialog open={urlOpen} onOpenChange={setUrlOpen} initialUrl={sharedUrl} autoImport={!!sharedUrl && !importUrlLocked} />
|
||||
<UpgradeDialog
|
||||
open={upgradeOpen}
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { VersionDiffView, type DiffSnapshot } from "@/components/recipe/version-diff-view";
|
||||
import { UpgradeDialog } from "@/components/premium/upgrade-dialog";
|
||||
import { ProBadge } from "@/components/premium/pro-badge";
|
||||
|
||||
type VersionSummary = {
|
||||
id: string;
|
||||
@@ -49,9 +51,14 @@ type VersionDetail = {
|
||||
export function VersionHistoryButton({
|
||||
recipeId,
|
||||
currentSnapshot,
|
||||
locked = false,
|
||||
}: {
|
||||
recipeId: string;
|
||||
currentSnapshot: DiffSnapshot;
|
||||
/** Available on some tier but not the viewer's — button still shows
|
||||
* (with a "Pro" upsell) and opens an upgrade prompt instead of the
|
||||
* version sheet. */
|
||||
locked?: boolean;
|
||||
}) {
|
||||
const t = useTranslations("recipe");
|
||||
const tForm = useTranslations("recipeForm");
|
||||
@@ -68,8 +75,13 @@ export function VersionHistoryButton({
|
||||
const [expandedData, setExpandedData] = useState<Record<string, VersionDetail>>({});
|
||||
const [restoringId, setRestoringId] = useState<string | null>(null);
|
||||
const [comparingId, setComparingId] = useState<string | null>(null);
|
||||
const [upgradeOpen, setUpgradeOpen] = useState(false);
|
||||
|
||||
async function handleOpen(isOpen: boolean) {
|
||||
if (isOpen && locked) {
|
||||
setUpgradeOpen(true);
|
||||
return;
|
||||
}
|
||||
setOpen(isOpen);
|
||||
if (isOpen) {
|
||||
setLoading(true);
|
||||
@@ -137,7 +149,10 @@ export function VersionHistoryButton({
|
||||
<History className="h-4 w-4" />
|
||||
</Button>
|
||||
} />
|
||||
<TooltipContent>{t("historyTooltip")}</TooltipContent>
|
||||
<TooltipContent className="flex items-center gap-1.5">
|
||||
{t("historyTooltip")}
|
||||
{locked && <ProBadge />}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Sheet open={open} onOpenChange={handleOpen}>
|
||||
@@ -247,6 +262,12 @@ export function VersionHistoryButton({
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<UpgradeDialog
|
||||
open={upgradeOpen}
|
||||
onOpenChange={setUpgradeOpen}
|
||||
featureKey="version_history"
|
||||
featureLabel="Recipe version history"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user