fix: remove lock-icon overlay from variations button, show Pro badge in tooltip instead (v0.78.3)
The lock icon overlapping the branch icon looked bad. Button is visible again when locked (not hidden) — click opens the upgrade dialog, tooltip shows a small "Pro" badge instead of an icon overlay. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
|
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
|
||||||
|
|
||||||
|
## 0.78.3 — 2026-07-24 15:00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Recipe variations button: removed the small lock icon that overlapped the branch icon when the feature was locked for your tier. The button is visible again (clicking opens an upgrade prompt) and the tooltip now shows a "Pro" badge instead.
|
||||||
|
|
||||||
## 0.78.2 — 2026-07-24 14:30
|
## 0.78.2 — 2026-07-24 14:30
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -116,7 +116,7 @@ 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 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` |
|
| 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`) |
|
| 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`) |
|
||||||
| Variations button lock-badge (closed 2026-07-24) | Exists | Was the one gated feature still shown disabled with a small lock-icon overlay + upgrade-dialog-on-click instead of hidden entirely; now hidden like every other gated feature, and the dead locked/`UpgradeDialog` code was removed from `VariationsButton`. | `apps/web/components/recipe/variations-button.tsx`, `apps/web/app/(app)/recipes/[id]/page.tsx` |
|
| Variations button locked treatment (revised 2026-07-24) | Exists | Only gated feature that stays visible-but-locked instead of hidden (deliberate, per explicit feedback: hiding it was tried first, then reverted). The lock-icon-overlapping-the-branch-icon look was removed; button renders normally, click opens `UpgradeDialog`, and the tooltip shows a small "Pro" badge instead of an icon overlay. | `apps/web/components/recipe/variations-button.tsx`, `apps/web/app/(app)/recipes/[id]/page.tsx` |
|
||||||
| Feature flags (per-tier) vs feature prefs (per-user cosmetic) | Exists, two distinct systems | Flags now gate 9 capabilities by tier (was 3): recipe_variations/drink_pairing/meal_pairing (default enabled) plus recipe_import_url, recipe_import_photo, nutrition_estimation, markdown_export, weekly_nutrition, grocery_delivery (2026-07-24, 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` |
|
| Feature flags (per-tier) vs feature prefs (per-user cosmetic) | Exists, two distinct systems | Flags now gate 9 capabilities by tier (was 3): recipe_variations/drink_pairing/meal_pairing (default enabled) plus recipe_import_url, recipe_import_photo, nutrition_estimation, markdown_export, weekly_nutrition, grocery_delivery (2026-07-24, 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` |
|
||||||
| **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`) |
|
| **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` |
|
| User webhooks (personal automation) | Exists | 7 events, Zapier-style, requires developer access | `apps/web/lib/webhooks.ts` |
|
||||||
|
|||||||
@@ -232,7 +232,6 @@ export default async function RecipePage({ params }: Params) {
|
|||||||
ingredients={recipe.ingredients.map((ing) => ({ rawName: ing.rawName }))}
|
ingredients={recipe.ingredients.map((ing) => ({ rawName: ing.rawName }))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!locked.variations && (
|
|
||||||
<VariationsButton
|
<VariationsButton
|
||||||
recipeId={id}
|
recipeId={id}
|
||||||
baseServings={recipe.baseServings}
|
baseServings={recipe.baseServings}
|
||||||
@@ -251,8 +250,8 @@ export default async function RecipePage({ params }: Params) {
|
|||||||
timerSeconds: s.timerSeconds,
|
timerSeconds: s.timerSeconds,
|
||||||
order: s.order,
|
order: s.order,
|
||||||
}))}
|
}))}
|
||||||
|
locked={locked.variations}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<ForkRecipeButton recipeId={id} variant={isOwner ? "duplicate" : "fork"} />
|
<ForkRecipeButton recipeId={id} variant={isOwner ? "duplicate" : "fork"} />
|
||||||
<ShareRecipeButton recipeId={id} visibility={recipe.visibility} />
|
<ShareRecipeButton recipeId={id} visibility={recipe.visibility} />
|
||||||
<SaveOfflineButton recipeId={id} recipeTitle={recipe.title} />
|
<SaveOfflineButton recipeId={id} recipeTitle={recipe.title} />
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function FeatureFlagsForm({
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="font-semibold text-lg">Feature Toggles</h2>
|
<h2 className="font-semibold text-lg">Feature Toggles</h2>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
Disable a feature for a tier to hide it entirely for that tier's users.
|
Disable a feature for a tier to hide it for that tier's users (most features hide entirely; recipe variations instead shows a "Pro" badge in its tooltip and opens an upgrade prompt — see each feature's actual behavior in the app).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { useState } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { GitBranch } from "lucide-react";
|
import { GitBranch } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { VariationsDialog } from "./variations-dialog";
|
import { VariationsDialog } from "./variations-dialog";
|
||||||
|
import { UpgradeDialog } from "@/components/premium/upgrade-dialog";
|
||||||
|
|
||||||
export function VariationsButton({
|
export function VariationsButton({
|
||||||
recipeId,
|
recipeId,
|
||||||
@@ -15,6 +17,7 @@ export function VariationsButton({
|
|||||||
cookMins,
|
cookMins,
|
||||||
ingredients,
|
ingredients,
|
||||||
steps,
|
steps,
|
||||||
|
locked = false,
|
||||||
}: {
|
}: {
|
||||||
recipeId: string;
|
recipeId: string;
|
||||||
baseServings: number;
|
baseServings: number;
|
||||||
@@ -23,20 +26,30 @@ export function VariationsButton({
|
|||||||
cookMins?: number | null;
|
cookMins?: number | null;
|
||||||
ingredients: Array<{ rawName: string; quantity?: string | number | null; unit?: string | null; note?: string | null; order: number }>;
|
ingredients: Array<{ rawName: string; quantity?: string | number | null; unit?: string | null; note?: string | null; order: number }>;
|
||||||
steps: Array<{ instruction: string; timerSeconds?: number | null; order: number }>;
|
steps: Array<{ instruction: string; timerSeconds?: number | null; order: number }>;
|
||||||
|
locked?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations("recipe");
|
const t = useTranslations("recipe");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [upgradeOpen, setUpgradeOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger render={
|
<TooltipTrigger render={
|
||||||
<Button variant="ghost" size="icon" onClick={() => setOpen(true)} aria-label={t("variationsTooltip")}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => (locked ? setUpgradeOpen(true) : setOpen(true))}
|
||||||
|
aria-label={t("variationsTooltip")}
|
||||||
|
>
|
||||||
<GitBranch className="h-4 w-4" />
|
<GitBranch className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
} />
|
} />
|
||||||
<TooltipContent>{t("variationsTooltip")}</TooltipContent>
|
<TooltipContent className="flex items-center gap-1.5">
|
||||||
|
{t("variationsTooltip")}
|
||||||
|
{locked && <Badge variant="secondary" className="text-[10px] px-1 py-0 leading-4">Pro</Badge>}
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<VariationsDialog
|
<VariationsDialog
|
||||||
@@ -50,6 +63,12 @@ export function VariationsButton({
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
/>
|
/>
|
||||||
|
<UpgradeDialog
|
||||||
|
open={upgradeOpen}
|
||||||
|
onOpenChange={setUpgradeOpen}
|
||||||
|
featureKey="recipe_variations"
|
||||||
|
featureLabel="Recipe variations"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mirrors CHANGELOG.md at the repo root — update both together.
|
// Mirrors CHANGELOG.md at the repo root — update both together.
|
||||||
export const APP_VERSION = "0.78.2";
|
export const APP_VERSION = "0.78.3";
|
||||||
|
|
||||||
export type ChangelogEntry = {
|
export type ChangelogEntry = {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: ChangelogEntry[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "0.78.3",
|
||||||
|
date: "2026-07-24 15:00",
|
||||||
|
fixed: [
|
||||||
|
"Recipe variations button: removed the small lock icon that overlapped the branch icon when the feature was locked for your tier. The button is visible again (clicking opens an upgrade prompt) and the tooltip now shows a \"Pro\" badge instead.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: "0.78.2",
|
version: "0.78.2",
|
||||||
date: "2026-07-24 14:30",
|
date: "2026-07-24 14:30",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@epicure/web",
|
"name": "@epicure/web",
|
||||||
"version": "0.78.2",
|
"version": "0.78.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "epicure",
|
"name": "epicure",
|
||||||
"version": "0.78.2",
|
"version": "0.78.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --filter web dev",
|
"dev": "pnpm --filter web dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user