feat: implement trial period for checkout sessions and enforce team plan member limit
Added a 14-day trial period for the "solo" and "team" plans during checkout session creation. Also, enforced a member limit of 3 for the "team" plan in the user invitation process and updated the settings page to reflect this limit with appropriate messaging.
This commit is contained in:
parent
9cd51e9f0b
commit
f6e5c39dcc
5 changed files with 50 additions and 8 deletions
|
|
@ -293,6 +293,9 @@ const createCheckoutSession = (
|
|||
customerId = customer.id;
|
||||
}
|
||||
|
||||
const TRIAL_PERIOD_DAYS = 14;
|
||||
const planHasTrial = plan === "solo" || plan === "team";
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
line_items: [
|
||||
|
|
@ -315,6 +318,7 @@ const createCheckoutSession = (
|
|||
organization_id: String(ownerContext.organizationId),
|
||||
checkout_plan: plan ?? "legacy_price",
|
||||
},
|
||||
...(planHasTrial && { trial_period_days: TRIAL_PERIOD_DAYS }),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -531,6 +531,20 @@ const inviteToOrganization = factory.createHandlers(async (c) => {
|
|||
return c.json({ error: "This email already belongs to another organization" }, 409);
|
||||
}
|
||||
|
||||
const TEAM_PLAN_MEMBER_LIMIT = 3;
|
||||
|
||||
if (
|
||||
billingState.active_subscription_plan === "team" &&
|
||||
billingState.member_count >= TEAM_PLAN_MEMBER_LIMIT
|
||||
) {
|
||||
return c.json(
|
||||
{
|
||||
error: `The Teams plan is limited to ${TEAM_PLAN_MEMBER_LIMIT} members per organization. Upgrade to the Founder plan to add unlimited members.`,
|
||||
},
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
if (billingState.is_trial_expired && billingState.active_subscription_plan !== "annual") {
|
||||
const requiredSeatsForInvite = billingState.member_count + 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ export default function SettingsPage() {
|
|||
);
|
||||
|
||||
const canManageMembers = organizationData?.is_billing_owner ?? false;
|
||||
const TEAM_PLAN_MEMBER_LIMIT = 3;
|
||||
const isAtTeamMemberLimit =
|
||||
organizationData?.active_subscription_plan === "team" &&
|
||||
(organizationData?.organization?.member_count ?? 0) >= TEAM_PLAN_MEMBER_LIMIT;
|
||||
|
||||
const getDisplayName = (input: {
|
||||
first_name?: string | null;
|
||||
|
|
@ -419,13 +423,21 @@ export default function SettingsPage() {
|
|||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
placeholder={t("settings:teamInvite.emailPlaceholder")}
|
||||
disabled={isAtTeamMemberLimit}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t("settings:teamInvite.hint")}</p>
|
||||
{isAtTeamMemberLimit ? (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||
Le plan Teams est limité à {TEAM_PLAN_MEMBER_LIMIT} membres. Passez au plan
|
||||
Founder pour ajouter des membres illimités.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">{t("settings:teamInvite.hint")}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
disabled={inviteOrganizationUserPending || !inviteEmail.trim()}
|
||||
disabled={inviteOrganizationUserPending || !inviteEmail.trim() || isAtTeamMemberLimit}
|
||||
onClick={() => {
|
||||
inviteOrganizationUser(inviteEmail.trim());
|
||||
setInviteEmail("");
|
||||
|
|
|
|||
|
|
@ -8,12 +8,17 @@ import { Label } from "@xtablo/ui/components/label";
|
|||
import { ArrowLeftIcon, MonitorIcon, MoonIcon, SparklesIcon, SunIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useSignUp } from "../hooks/auth";
|
||||
import { DEFAULT_SIGNUP_BILLING_INTENT, type SignupBillingIntent } from "../lib/billing";
|
||||
import {
|
||||
DEFAULT_SIGNUP_BILLING_INTENT,
|
||||
isSignupBillingIntent,
|
||||
type SignupBillingIntent,
|
||||
} from "../lib/billing";
|
||||
|
||||
export function SignUpV2Page() {
|
||||
const { t } = useTranslation(["auth", "common"]);
|
||||
const [searchParams] = useSearchParams();
|
||||
const redirectUrl = localStorage.getItem("redirectUrl");
|
||||
const { mutate: signUp, isPending } = useSignUp({
|
||||
redirectUrl: redirectUrl ?? null,
|
||||
|
|
@ -30,8 +35,9 @@ export function SignUpV2Page() {
|
|||
business_name: "",
|
||||
});
|
||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||
const planParam = searchParams.get("plan");
|
||||
const [billingIntent, setBillingIntent] = useState<SignupBillingIntent>(
|
||||
DEFAULT_SIGNUP_BILLING_INTENT
|
||||
isSignupBillingIntent(planParam) ? planParam : DEFAULT_SIGNUP_BILLING_INTENT
|
||||
);
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
|
|
|||
|
|
@ -8,13 +8,18 @@ import { Label } from "@xtablo/ui/components/label";
|
|||
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignUp } from "../hooks/auth";
|
||||
import { DEFAULT_SIGNUP_BILLING_INTENT, type SignupBillingIntent } from "../lib/billing";
|
||||
import {
|
||||
DEFAULT_SIGNUP_BILLING_INTENT,
|
||||
isSignupBillingIntent,
|
||||
type SignupBillingIntent,
|
||||
} from "../lib/billing";
|
||||
|
||||
export function SignUpPage() {
|
||||
const { t } = useTranslation(["auth", "common"]);
|
||||
const [searchParams] = useSearchParams();
|
||||
const redirectUrl = localStorage.getItem("redirectUrl");
|
||||
const { mutate: signUp, isPending } = useSignUp({
|
||||
redirectUrl: redirectUrl ?? null,
|
||||
|
|
@ -31,8 +36,9 @@ export function SignUpPage() {
|
|||
business_name: "",
|
||||
});
|
||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||
const planParam = searchParams.get("plan");
|
||||
const [billingIntent, setBillingIntent] = useState<SignupBillingIntent>(
|
||||
DEFAULT_SIGNUP_BILLING_INTENT
|
||||
isSignupBillingIntent(planParam) ? planParam : DEFAULT_SIGNUP_BILLING_INTENT
|
||||
);
|
||||
|
||||
// 3D Parallax effect
|
||||
|
|
|
|||
Loading…
Reference in a new issue