Compare commits

..

No commits in common. "a8c3fd6997672bc578e39d8a28a7f7849b42e75c" and "cb7f8479e40598e576b8623912f2ca90446e165c" have entirely different histories.

5 changed files with 12 additions and 29 deletions

View file

@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
import { Lock, Split } from "lucide-react"; import { Lock, Split } from "lucide-react";
import type { TransactionRow } from "../../shared/types"; import type { TransactionRow } from "../../shared/types";
import { requiredTierFor } from "../../shared/entitlements"; import { requiredTierFor } from "../../shared/entitlements";
import { SPLIT_FEATURE, type SplitAccess } from "../../shared/splitGate"; import type { SplitAccess } from "../../shared/splitGate";
interface SplitButtonProps { interface SplitButtonProps {
row: TransactionRow; row: TransactionRow;
@ -40,7 +40,7 @@ export default function SplitButton({ row, access, onOpenSplit, onOpenUpsell }:
title={ title={
locked locked
? t("transactions.splitLocked", { ? t("transactions.splitLocked", {
tier: t(`license.editions.${requiredTierFor(SPLIT_FEATURE)}`), tier: t(`license.editions.${requiredTierFor("adjustments")}`),
}) })
: t("transactions.splitAdjustment") : t("transactions.splitAdjustment")
} }

View file

@ -8,7 +8,7 @@ import type {
SplitChild, SplitChild,
} from "../../shared/types"; } from "../../shared/types";
import type { LinkedTransferTooltipRow } from "../../services/balance.service"; import type { LinkedTransferTooltipRow } from "../../services/balance.service";
import { SPLIT_FEATURE, type SplitAccess } from "../../shared/splitGate"; import type { SplitAccess } from "../../shared/splitGate";
import { onboardingTargetAttributes } from "../../utils/onboardingPath"; import { onboardingTargetAttributes } from "../../utils/onboardingPath";
import CategoryCombobox from "../shared/CategoryCombobox"; import CategoryCombobox from "../shared/CategoryCombobox";
import UpsellDialog from "../shared/UpsellDialog"; import UpsellDialog from "../shared/UpsellDialog";
@ -28,7 +28,7 @@ interface TransactionTableProps {
onDeleteSplit: (parentId: number) => Promise<void>; onDeleteSplit: (parentId: number) => Promise<void>;
/** /**
* What the Répartition button does (#369): `splitAccessFor` over * What the Répartition button does (#369): `splitAccessFor` over
* `useEntitlement(SPLIT_FEATURE)`. Required on purpose: a default of "open" * `useEntitlement("adjustments")`. Required on purpose: a default of "open"
* would let a new caller leave the split ungated without a word. * would let a new caller leave the split ungated without a word.
*/ */
splitAccess: SplitAccess; splitAccess: SplitAccess;
@ -351,7 +351,7 @@ export default function TransactionTable({
)} )}
{/* Symmetric: the upsell goes away once the edition resolves to Base or Premium. */} {/* Symmetric: the upsell goes away once the edition resolves to Base or Premium. */}
{showSplitUpsell && splitAccess !== "open" && ( {showSplitUpsell && splitAccess !== "open" && (
<UpsellDialog feature={SPLIT_FEATURE} onClose={() => setShowSplitUpsell(false)} /> <UpsellDialog feature="adjustments" onClose={() => setShowSplitUpsell(false)} />
)} )}
</div> </div>
); );

View file

@ -119,24 +119,15 @@ describe("TransactionTable — the Répartition button's wiring (#369)", () => {
expect(upsells(opened)).toHaveLength(0); expect(upsells(opened)).toHaveLength(0);
}); });
// One click arms one window: an edition change never brings up the other one.
it("an open split window survives a license refresh and closes once the edition is Free", () => { it("an open split window survives a license refresh and closes once the edition is Free", () => {
(splitButtons(render("open"))[0].props.onOpenSplit as () => void)(); (splitButtons(render("open"))[0].props.onOpenSplit as () => void)();
const refreshing = render("pending"); expect(splitWindows(render("pending"))).toHaveLength(1);
expect(splitWindows(refreshing)).toHaveLength(1); expect(splitWindows(render("locked"))).toHaveLength(0);
expect(upsells(refreshing)).toHaveLength(0);
const downgraded = render("locked");
expect(splitWindows(downgraded)).toHaveLength(0);
expect(upsells(downgraded)).toHaveLength(0);
}); });
it("an open upsell survives a license refresh and goes away once the edition is Base", () => { it("an open upsell survives a license refresh and goes away once the edition is Base", () => {
(splitButtons(render("locked"))[0].props.onOpenUpsell as () => void)(); (splitButtons(render("locked"))[0].props.onOpenUpsell as () => void)();
const refreshing = render("pending"); expect(upsells(render("pending"))).toHaveLength(1);
expect(upsells(refreshing)).toHaveLength(1); expect(upsells(render("open"))).toHaveLength(0);
expect(splitWindows(refreshing)).toHaveLength(0);
const upgraded = render("open");
expect(upsells(upgraded)).toHaveLength(0);
expect(splitWindows(upgraded)).toHaveLength(0);
}); });
}); });

View file

@ -7,7 +7,7 @@ import EmptyState from "../components/shared/EmptyState";
import OnboardingStepCard from "../components/onboarding/OnboardingStepCard"; import OnboardingStepCard from "../components/onboarding/OnboardingStepCard";
import { useOnboarding } from "../contexts/OnboardingContext"; import { useOnboarding } from "../contexts/OnboardingContext";
import { useEntitlement } from "../hooks/useEntitlement"; import { useEntitlement } from "../hooks/useEntitlement";
import { SPLIT_FEATURE, splitAccessFor } from "../shared/splitGate"; import { splitAccessFor } from "../shared/splitGate";
import { import {
readOnboardingNavState, readOnboardingNavState,
showsStepCard, showsStepCard,
@ -44,7 +44,7 @@ export default function TransactionsPage() {
const onboarding = useOnboarding(); const onboarding = useOnboarding();
// The Répartition button belongs to the Base edition (#369): locked with the // 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. // upsell on Free, inert while the license is not ready. The page itself stays Free.
const splitGate = useEntitlement(SPLIT_FEATURE); const splitGate = useEntitlement("adjustments");
const [resultMessage, setResultMessage] = useState<string | null>(null); const [resultMessage, setResultMessage] = useState<string | null>(null);
const [menu, setMenu] = useState<{ x: number; y: number; row: TransactionRow } | null>(null); const [menu, setMenu] = useState<{ x: number; y: number; row: TransactionRow } | null>(null);
const [pending, setPending] = useState<TransactionRow | null>(null); const [pending, setPending] = useState<TransactionRow | null>(null);

View file

@ -19,19 +19,11 @@
* license refresh — never locked, never open on a fail-closed `allowed`. * license refresh — never locked, never open on a fail-closed `allowed`.
*/ */
import type { EntitlementGate } from "./profileGate"; 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. */ /** What the split button does: open the split window, show the upsell, or nothing yet. */
export type SplitAccess = "open" | "locked" | "pending"; export type SplitAccess = "open" | "locked" | "pending";
/** The split button's access, from the gate `useEntitlement(SPLIT_FEATURE)` returns. */ /** The split button's access, from the gate `useEntitlement("adjustments")` returns. */
export function splitAccessFor(gate: EntitlementGate): SplitAccess { export function splitAccessFor(gate: EntitlementGate): SplitAccess {
if (!gate.ready) return "pending"; if (!gate.ready) return "pending";
return gate.allowed ? "open" : "locked"; return gate.allowed ? "open" : "locked";