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 { 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 type { SplitAccess } from "../../shared/splitGate";
|
import { SPLIT_FEATURE, 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("adjustments")}`),
|
tier: t(`license.editions.${requiredTierFor(SPLIT_FEATURE)}`),
|
||||||
})
|
})
|
||||||
: t("transactions.splitAdjustment")
|
: t("transactions.splitAdjustment")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 type { SplitAccess } from "../../shared/splitGate";
|
import { SPLIT_FEATURE, 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("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.
|
* 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="adjustments" onClose={() => setShowSplitUpsell(false)} />
|
<UpsellDialog feature={SPLIT_FEATURE} onClose={() => setShowSplitUpsell(false)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -119,15 +119,24 @@ 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)();
|
||||||
expect(splitWindows(render("pending"))).toHaveLength(1);
|
const refreshing = render("pending");
|
||||||
expect(splitWindows(render("locked"))).toHaveLength(0);
|
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", () => {
|
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)();
|
||||||
expect(upsells(render("pending"))).toHaveLength(1);
|
const refreshing = render("pending");
|
||||||
expect(upsells(render("open"))).toHaveLength(0);
|
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 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 { splitAccessFor } from "../shared/splitGate";
|
import { SPLIT_FEATURE, 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("adjustments");
|
const splitGate = useEntitlement(SPLIT_FEATURE);
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,19 @@
|
||||||
* 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("adjustments")` returns. */
|
/** The split button's access, from the gate `useEntitlement(SPLIT_FEATURE)` 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";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue