ce7741c0b6
UpgradeDialog's CTA now sends users to Settings > Billing (with a feature-specific banner) instead of a generic support-ticket prefill, and fires a best-effort tracking beacon recording which feature triggered the click. New feature_upgrade_clicks table + Admin > Insights chart ("Most-requested locked features") gives admins visibility into which locked features actually drive upgrade interest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
164 lines
6.0 KiB
TypeScript
164 lines
6.0 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { Sparkles, Check } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
|
|
// Marketing copy per feature — kept client-side (not sourced from
|
|
// lib/feature-flags.ts's FEATURE_DEFINITIONS) since that module pulls in
|
|
// the DB client for server-only helpers and can't be bundled for the
|
|
// browser. Falls back to a generic pitch for any key not listed here.
|
|
const FEATURE_COPY: Record<string, { tagline: string; bullets: string[] }> = {
|
|
recipe_variations: {
|
|
tagline: "Get AI-generated twists on any recipe you save — dietary swaps, flavor changes, or a whole new spin — without starting from scratch.",
|
|
bullets: [
|
|
"Dietary swaps (vegan, gluten-free, dairy-free, and more)",
|
|
"Flavor variations generated from your existing recipe",
|
|
"Saved as a new recipe, linked back to the original",
|
|
],
|
|
},
|
|
drink_pairing: {
|
|
tagline: "Never wonder what to serve alongside dinner again — get drink pairings suggested for any dish.",
|
|
bullets: [
|
|
"Wine, beer, cocktail, and non-alcoholic suggestions",
|
|
"Tailored to the specific dish, not generic pairing charts",
|
|
"Explains why each pairing works",
|
|
],
|
|
},
|
|
meal_pairing: {
|
|
tagline: "Turn a single recipe into a full meal — get side dish, salad, and sauce suggestions that actually complement it.",
|
|
bullets: [
|
|
"Starters, sides, salads, breads, and sauces suggested per dish",
|
|
"One click to generate and save any suggestion as its own recipe",
|
|
"Pick multiple pairings and generate them all at once",
|
|
],
|
|
},
|
|
recipe_import_url: {
|
|
tagline: "Stop retyping recipes from other sites — paste a link and get a fully structured recipe in seconds.",
|
|
bullets: [
|
|
"Works with most recipe blogs and sites",
|
|
"Extracts ingredients, steps, servings, and timing automatically",
|
|
"Edit anything before saving",
|
|
],
|
|
},
|
|
recipe_import_photo: {
|
|
tagline: "Digitize a cookbook page or handwritten family recipe just by photographing it.",
|
|
bullets: [
|
|
"Recognizes printed or handwritten recipes",
|
|
"Extracts ingredients and steps automatically",
|
|
"Saved as a private, editable recipe",
|
|
],
|
|
},
|
|
nutrition_estimation: {
|
|
tagline: "Know what's in every dish — get calorie and macro estimates for any recipe, no manual entry required.",
|
|
bullets: [
|
|
"Calories, protein, carbs, fat, fiber, and sodium per serving",
|
|
"Combines AI estimation with USDA reference data",
|
|
"Powers your weekly nutrition rollup on the meal plan",
|
|
],
|
|
},
|
|
markdown_export: {
|
|
tagline: "Take your recipes, meal plans, and shopping lists anywhere — export clean Markdown you can paste into any note app.",
|
|
bullets: [
|
|
"Works on recipes, collections, meal plans, and pantry lists",
|
|
"Copy to clipboard or download as a .md file",
|
|
"Clean formatting, no app lock-in",
|
|
],
|
|
},
|
|
weekly_nutrition: {
|
|
tagline: "See how your whole week stacks up against your nutrition goals, not just one meal at a time.",
|
|
bullets: [
|
|
"Daily average calories and macros across your meal plan week",
|
|
"Compared directly against your saved nutrition goals",
|
|
"Flags days with missing nutrition data",
|
|
],
|
|
},
|
|
version_history: {
|
|
tagline: "Never lose a good version of a recipe — every edit is saved, so you can compare or roll back anytime.",
|
|
bullets: [
|
|
"Every save keeps a full snapshot of the prior version",
|
|
"Side-by-side diff view to see exactly what changed",
|
|
"Restore any past version with one click",
|
|
],
|
|
},
|
|
grocery_delivery: {
|
|
tagline: "Skip the manual shopping trip — send your list straight to a grocery delivery provider.",
|
|
bullets: [
|
|
"One click from your shopping list to checkout",
|
|
"Keeps quantities and aisle grouping intact",
|
|
"No re-typing your list into another app",
|
|
],
|
|
},
|
|
};
|
|
|
|
export function UpgradeDialog({
|
|
open,
|
|
onOpenChange,
|
|
featureKey,
|
|
featureLabel,
|
|
}: {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
featureKey: string;
|
|
featureLabel: string;
|
|
}) {
|
|
const copy = FEATURE_COPY[featureKey];
|
|
const router = useRouter();
|
|
|
|
function handleUpgradeClick() {
|
|
// keepalive lets this best-effort beacon survive the navigation that
|
|
// follows immediately after — a plain fetch would otherwise risk being
|
|
// aborted mid-flight by the router.push below.
|
|
fetch("/api/v1/users/me/upgrade-interest", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ featureKey }),
|
|
keepalive: true,
|
|
}).catch(() => {});
|
|
onOpenChange(false);
|
|
router.push(`/settings/billing?upgrade=${encodeURIComponent(featureKey)}`);
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-sm">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Sparkles className="h-5 w-5 text-primary" />
|
|
{featureLabel}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{copy?.tagline ?? `${featureLabel} is available on the Pro plan. Free accounts don't include it.`}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{copy && (
|
|
<ul className="space-y-1.5">
|
|
{copy.bullets.map((bullet) => (
|
|
<li key={bullet} className="flex items-start gap-2 text-sm">
|
|
<Check className="h-4 w-4 text-primary shrink-0 mt-0.5" />
|
|
<span>{bullet}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
<p className="text-sm text-muted-foreground">
|
|
Included on the Pro plan (€4.99/mo).
|
|
</p>
|
|
<DialogFooter className="sm:justify-start">
|
|
<Button onClick={handleUpgradeClick}>
|
|
See Pro plans
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|