feat(gating): reserve transaction splits for the Base edition #452

Merged
maximus merged 3 commits from issue-369-split-base-edition into main 2026-10-17 11:40:14 +00:00
4 changed files with 16 additions and 8 deletions
Showing only changes of commit a8c3fd6997 - Show all commits

View file

@ -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")
}

View file

@ -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<void>;
/**
* 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" && (
<UpsellDialog feature="adjustments" onClose={() => setShowSplitUpsell(false)} />
<UpsellDialog feature={SPLIT_FEATURE} onClose={() => setShowSplitUpsell(false)} />
)}
</div>
);

View file

@ -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<string | null>(null);
const [menu, setMenu] = useState<{ x: number; y: number; row: TransactionRow } | null>(null);
const [pending, setPending] = useState<TransactionRow | null>(null);

View file

@ -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";