Compare commits
2 commits
cb7f8479e4
...
a8c3fd6997
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8c3fd6997 | ||
|
|
d5d0e89d2c |
5 changed files with 29 additions and 12 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -119,15 +119,24 @@ describe("TransactionTable — the Répartition button's wiring (#369)", () => {
|
|||
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", () => {
|
||||
(splitButtons(render("open"))[0].props.onOpenSplit as () => void)();
|
||||
expect(splitWindows(render("pending"))).toHaveLength(1);
|
||||
expect(splitWindows(render("locked"))).toHaveLength(0);
|
||||
const refreshing = render("pending");
|
||||
expect(splitWindows(refreshing)).toHaveLength(1);
|
||||
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", () => {
|
||||
(splitButtons(render("locked"))[0].props.onOpenUpsell as () => void)();
|
||||
expect(upsells(render("pending"))).toHaveLength(1);
|
||||
expect(upsells(render("open"))).toHaveLength(0);
|
||||
const refreshing = render("pending");
|
||||
expect(upsells(refreshing)).toHaveLength(1);
|
||||
expect(splitWindows(refreshing)).toHaveLength(0);
|
||||
const upgraded = render("open");
|
||||
expect(upsells(upgraded)).toHaveLength(0);
|
||||
expect(splitWindows(upgraded)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in a new issue