From d158a204af4d22f13cbc471950310b937dea3a79 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Mon, 24 Nov 2025 23:09:14 +0100 Subject: [PATCH] Add beta plan + upsell modals + block past_due users --- apps/main/src/App.tsx | 15 +- apps/main/src/components/NavigationBar.tsx | 84 +++++++++++ .../src/components/ProtectedRoute.test.tsx | 1 + apps/main/src/components/SubscriptionCard.tsx | 52 +++++-- apps/main/src/components/TrialUpsellModal.tsx | 134 ++++++++++++++++++ apps/main/src/components/UpgradePanel.tsx | 115 +++++++++++++++ .../main/src/contexts/UpgradeBlockContext.tsx | 28 ++++ apps/main/src/hooks/stripe.ts | 52 ++++++- .../src/providers/UserStoreProvider.test.tsx | 1 + apps/main/src/utils/testHelpers.tsx | 1 + apps/main/stats.html | 2 +- packages/shared-types/src/database.types.ts | 38 ++--- .../20251124211154_add_beta_plan.sql | 7 + ...51124212932_add_created_at_to_profiles.sql | 8 ++ xtablo-expo/lib/database.types.ts | 7 +- 15 files changed, 502 insertions(+), 43 deletions(-) create mode 100644 apps/main/src/components/TrialUpsellModal.tsx create mode 100644 apps/main/src/components/UpgradePanel.tsx create mode 100644 apps/main/src/contexts/UpgradeBlockContext.tsx create mode 100644 supabase/migrations/20251124211154_add_beta_plan.sql create mode 100644 supabase/migrations/20251124212932_add_created_at_to_profiles.sql diff --git a/apps/main/src/App.tsx b/apps/main/src/App.tsx index 707991c..6fbbe7d 100644 --- a/apps/main/src/App.tsx +++ b/apps/main/src/App.tsx @@ -3,6 +3,9 @@ import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext"; import { Toaster } from "@xtablo/ui/components/sonner"; import { BrowserRouter as Router, useRoutes } from "react-router-dom"; import { CookieBanner } from "./components/CookieBanner"; +import { TrialUpsellModal } from "./components/TrialUpsellModal"; +import { UpgradePanel } from "./components/UpgradePanel"; +import { UpgradeBlockProvider } from "./contexts/UpgradeBlockContext"; import { useCookieConsent } from "./hooks/useCookieConsent"; import { useDatadogRumViewName } from "./hooks/useDatadogRumViewName"; import { publicRoutes } from "./lib/publicRoutes"; @@ -21,9 +24,17 @@ const Routes = () => { return publicElement; } - // If app route matched, render it inside UserStoreProvider + // If app route matched, render it inside UserStoreProvider and UpgradeBlockProvider if (appElement) { - return {appElement}; + return ( + + + + + {appElement} + + + ); } // Neither matched, show 404 diff --git a/apps/main/src/components/NavigationBar.tsx b/apps/main/src/components/NavigationBar.tsx index ba99c44..925a28c 100644 --- a/apps/main/src/components/NavigationBar.tsx +++ b/apps/main/src/components/NavigationBar.tsx @@ -13,10 +13,12 @@ import { import { TypographyLarge, TypographyMuted } from "@xtablo/ui/components/typography"; import { cva, type VariantProps } from "class-variance-authority"; import { + AlertCircle, CalendarCheckIcon, CalendarIcon, Circle, ConstructionIcon, + CreditCard, FileTextIcon, Kanban, LogOutIcon, @@ -26,6 +28,7 @@ import { PlusIcon, SendIcon, SettingsIcon, + Sparkles, SquareKanban, } from "lucide-react"; import { useState } from "react"; @@ -34,6 +37,7 @@ import { useTranslation } from "react-i18next"; import { Link as RouterLink, useLocation } from "react-router-dom"; import { twMerge } from "tailwind-merge"; import { useLogout } from "../hooks/auth"; +import { useCreateCheckoutSession, useTrialExpiration } from "../hooks/stripe"; import { isProd, isStaging } from "../lib/env"; import { useIsReadOnlyUser, useUser } from "../providers/UserStoreProvider"; import { getXtabloIcon } from "../utils/iconHelpers"; @@ -279,7 +283,16 @@ export const SideNavigation = ({ isMobileMenuOpen }: { isMobileMenuOpen: boolean export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) { const location = useLocation(); const isReadOnly = useIsReadOnlyUser(); + const user = useUser(); const { t } = useTranslation("navigation"); + const { daysRemaining } = useTrialExpiration(); + const { mutate: createCheckout, isPending: checkoutPending } = useCreateCheckoutSession(); + + const STANDARD_MONTHLY_PRICE_ID = import.meta.env.VITE_STRIPE_STANDARD_MONTHLY_PRICE_ID || ""; + + // Show upsell for users in trial period (not beta, not paid, and daysRemaining exists) + const shouldShowUpsell = daysRemaining !== null && user.plan === "none" && !user.is_temporary; + const isUrgent = daysRemaining !== null && daysRemaining <= 3; type List = T[]; @@ -391,6 +404,77 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) { })}