From 8fc463313d7bc64078bac57417271e91bf8d5e35 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 8 Mar 2026 21:59:38 +0100 Subject: [PATCH] Fix trial upsell modal logic --- .../src/components/TrialUpsellModal.test.ts | 59 +++++++++++++++++++ apps/main/src/components/TrialUpsellModal.tsx | 36 +++++++++-- 2 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 apps/main/src/components/TrialUpsellModal.test.ts diff --git a/apps/main/src/components/TrialUpsellModal.test.ts b/apps/main/src/components/TrialUpsellModal.test.ts new file mode 100644 index 0000000..9e07e9a --- /dev/null +++ b/apps/main/src/components/TrialUpsellModal.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { shouldShowTrialUpsell } from "./TrialUpsellModal"; + +describe("shouldShowTrialUpsell", () => { + it("does not show when trial has more than 7 days remaining", () => { + expect( + shouldShowTrialUpsell({ + isTrialExpired: false, + activeSubscriptionPlan: null, + isTemporaryUser: false, + daysRemaining: 14, + }) + ).toBe(false); + }); + + it("shows when trial is within reminder window and user has no subscription", () => { + expect( + shouldShowTrialUpsell({ + isTrialExpired: false, + activeSubscriptionPlan: null, + isTemporaryUser: false, + daysRemaining: 3, + }) + ).toBe(true); + }); + + it("does not show when trial is expired", () => { + expect( + shouldShowTrialUpsell({ + isTrialExpired: true, + activeSubscriptionPlan: null, + isTemporaryUser: false, + daysRemaining: null, + }) + ).toBe(false); + }); + + it("does not show when organization already has a paid subscription", () => { + expect( + shouldShowTrialUpsell({ + isTrialExpired: false, + activeSubscriptionPlan: "team", + isTemporaryUser: false, + daysRemaining: 2, + }) + ).toBe(false); + }); + + it("does not show for temporary users", () => { + expect( + shouldShowTrialUpsell({ + isTrialExpired: false, + activeSubscriptionPlan: null, + isTemporaryUser: true, + daysRemaining: 2, + }) + ).toBe(false); + }); +}); diff --git a/apps/main/src/components/TrialUpsellModal.tsx b/apps/main/src/components/TrialUpsellModal.tsx index d74ab24..032e0ef 100644 --- a/apps/main/src/components/TrialUpsellModal.tsx +++ b/apps/main/src/components/TrialUpsellModal.tsx @@ -15,9 +15,29 @@ import { useMaybeUser } from "../providers/UserStoreProvider"; const MODAL_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes const LAST_SHOWN_KEY = "trial-upsell-modal-last-shown"; +const TRIAL_UPSELL_REMINDER_DAYS = 7; + +export const shouldShowTrialUpsell = (input: { + isTrialExpired: boolean; + activeSubscriptionPlan: "solo" | "team" | "annual" | null; + isTemporaryUser: boolean; + daysRemaining: number | null; +}) => { + const { isTrialExpired, activeSubscriptionPlan, isTemporaryUser, daysRemaining } = input; + + if (isTrialExpired || activeSubscriptionPlan || isTemporaryUser) { + return false; + } + + if (daysRemaining === null) { + return false; + } + + return daysRemaining > 0 && daysRemaining <= TRIAL_UPSELL_REMINDER_DAYS; +}; /** - * Auto-opening modal that shows every 15 minutes to remind users about trial expiration. + * Auto-opening modal that reminds users near trial expiration. */ export function TrialUpsellModal() { const [isOpen, setIsOpen] = useState(false); @@ -26,11 +46,15 @@ export function TrialUpsellModal() { const user = useMaybeUser(); const { mutate: createCheckout, isPending: checkoutPending } = useCreateCheckoutSession(); - const shouldShowModal = - !!organizationData && - !organizationData.is_trial_expired && - !organizationData.active_subscription_plan && - !user?.is_temporary; + const shouldShowModal = Boolean( + organizationData && + shouldShowTrialUpsell({ + isTrialExpired: organizationData.is_trial_expired, + activeSubscriptionPlan: organizationData.active_subscription_plan, + isTemporaryUser: Boolean(user?.is_temporary), + daysRemaining, + }) + ); const requiredPlan = organizationData?.required_plan ?? "solo"; const checkoutPlan = requiredPlan === "team" ? "team" : "solo";