From a8c3fd6997672bc578e39d8a28a7f7849b42e75c Mon Sep 17 00:00:00 2001 From: le king fu Date: Sat, 17 Oct 2026 07:36:33 -0400 Subject: [PATCH] refactor(gating): one constant for the split's feature key Review of PR #452 (comment 24129): "adjustments" was written three times for the split (the page's gate, the button's edition, the dialog's locked screen). SPLIT_FEATURE in splitGate.ts is now the one source, so moving the split to its own key or to Premium is a one-line change. No behaviour change; setting it to "budget" turns 2 tests red. Refs #369 Co-Authored-By: Claude Opus 5.5 --- src/components/transactions/SplitButton.tsx | 4 ++-- src/components/transactions/TransactionTable.tsx | 6 +++--- src/pages/TransactionsPage.tsx | 4 ++-- src/shared/splitGate.ts | 10 +++++++++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/transactions/SplitButton.tsx b/src/components/transactions/SplitButton.tsx index d244b3e..09266cd 100644 --- a/src/components/transactions/SplitButton.tsx +++ b/src/components/transactions/SplitButton.tsx @@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next"; import { Lock, Split } from "lucide-react"; import type { TransactionRow } from "../../shared/types"; import { requiredTierFor } from "../../shared/entitlements"; -import type { SplitAccess } from "../../shared/splitGate"; +import { SPLIT_FEATURE, type SplitAccess } from "../../shared/splitGate"; interface SplitButtonProps { row: TransactionRow; @@ -40,7 +40,7 @@ export default function SplitButton({ row, access, onOpenSplit, onOpenUpsell }: title={ locked ? t("transactions.splitLocked", { - tier: t(`license.editions.${requiredTierFor("adjustments")}`), + tier: t(`license.editions.${requiredTierFor(SPLIT_FEATURE)}`), }) : t("transactions.splitAdjustment") } diff --git a/src/components/transactions/TransactionTable.tsx b/src/components/transactions/TransactionTable.tsx index b01273b..3ad7034 100644 --- a/src/components/transactions/TransactionTable.tsx +++ b/src/components/transactions/TransactionTable.tsx @@ -8,7 +8,7 @@ import type { SplitChild, } from "../../shared/types"; import type { LinkedTransferTooltipRow } from "../../services/balance.service"; -import type { SplitAccess } from "../../shared/splitGate"; +import { SPLIT_FEATURE, type SplitAccess } from "../../shared/splitGate"; import { onboardingTargetAttributes } from "../../utils/onboardingPath"; import CategoryCombobox from "../shared/CategoryCombobox"; import UpsellDialog from "../shared/UpsellDialog"; @@ -28,7 +28,7 @@ interface TransactionTableProps { onDeleteSplit: (parentId: number) => Promise; /** * What the Répartition button does (#369): `splitAccessFor` over - * `useEntitlement("adjustments")`. Required on purpose: a default of "open" + * `useEntitlement(SPLIT_FEATURE)`. Required on purpose: a default of "open" * would let a new caller leave the split ungated without a word. */ splitAccess: SplitAccess; @@ -351,7 +351,7 @@ export default function TransactionTable({ )} {/* Symmetric: the upsell goes away once the edition resolves to Base or Premium. */} {showSplitUpsell && splitAccess !== "open" && ( - setShowSplitUpsell(false)} /> + setShowSplitUpsell(false)} /> )} ); diff --git a/src/pages/TransactionsPage.tsx b/src/pages/TransactionsPage.tsx index 02bf514..4a21331 100644 --- a/src/pages/TransactionsPage.tsx +++ b/src/pages/TransactionsPage.tsx @@ -7,7 +7,7 @@ import EmptyState from "../components/shared/EmptyState"; import OnboardingStepCard from "../components/onboarding/OnboardingStepCard"; import { useOnboarding } from "../contexts/OnboardingContext"; import { useEntitlement } from "../hooks/useEntitlement"; -import { splitAccessFor } from "../shared/splitGate"; +import { SPLIT_FEATURE, splitAccessFor } from "../shared/splitGate"; import { readOnboardingNavState, showsStepCard, @@ -44,7 +44,7 @@ export default function TransactionsPage() { const onboarding = useOnboarding(); // The Répartition button belongs to the Base edition (#369): locked with the // upsell on Free, inert while the license is not ready. The page itself stays Free. - const splitGate = useEntitlement("adjustments"); + const splitGate = useEntitlement(SPLIT_FEATURE); const [resultMessage, setResultMessage] = useState(null); const [menu, setMenu] = useState<{ x: number; y: number; row: TransactionRow } | null>(null); const [pending, setPending] = useState(null); diff --git a/src/shared/splitGate.ts b/src/shared/splitGate.ts index f6c0c47..b8795c3 100644 --- a/src/shared/splitGate.ts +++ b/src/shared/splitGate.ts @@ -19,11 +19,19 @@ * license refresh — never locked, never open on a fail-closed `allowed`. */ import type { EntitlementGate } from "./profileGate"; +import type { FeatureKey } from "./entitlements"; + +/** + * The feature the split belongs to. The page reads its gate, the button names + * its edition and the dialog shows its locked screen from this one constant, so + * moving the split (to its own key, to Premium) is a one-line change. + */ +export const SPLIT_FEATURE: FeatureKey = "adjustments"; /** What the split button does: open the split window, show the upsell, or nothing yet. */ export type SplitAccess = "open" | "locked" | "pending"; -/** The split button's access, from the gate `useEntitlement("adjustments")` returns. */ +/** The split button's access, from the gate `useEntitlement(SPLIT_FEATURE)` returns. */ export function splitAccessFor(gate: EntitlementGate): SplitAccess { if (!gate.ready) return "pending"; return gate.allowed ? "open" : "locked";