diff --git a/packages/backend/.env.example b/packages/backend/.env.example index c1ab18f..141cf5f 100644 --- a/packages/backend/.env.example +++ b/packages/backend/.env.example @@ -16,4 +16,6 @@ STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key STRIPE_SECRET_KEY=your_stripe_secret_key RESEND_API_KEY=re_xxxxxxxxxxxxxxxx -EMAIL_FROM=Sprint \ No newline at end of file +EMAIL_FROM=Sprint + +SEED_PASSWORD=replace-in-production \ No newline at end of file diff --git a/packages/backend/src/routes/issue/create.ts b/packages/backend/src/routes/issue/create.ts index 274106c..415525b 100644 --- a/packages/backend/src/routes/issue/create.ts +++ b/packages/backend/src/routes/issue/create.ts @@ -2,11 +2,11 @@ import { IssueCreateRequestSchema } from "@sprint/shared"; import type { AuthedRequest } from "../../auth/middleware"; import { createIssue, - FREE_TIER_LIMITS, - getOrganisationIssueCount, + // FREE_TIER_LIMITS, + // getOrganisationIssueCount, getOrganisationMemberRole, getProjectByID, - getUserById, + // getUserById, } from "../../db/queries"; import { errorResponse, parseJsonBody } from "../../validation"; @@ -34,17 +34,17 @@ export default async function issueCreate(req: AuthedRequest) { } // check free tier limit - const user = await getUserById(req.userId); - if (user && user.plan !== "pro") { - const issueCount = await getOrganisationIssueCount(project.organisationId); - if (issueCount >= FREE_TIER_LIMITS.issuesPerOrganisation) { - return errorResponse( - `free tier is limited to ${FREE_TIER_LIMITS.issuesPerOrganisation} issues per organisation. upgrade to pro for unlimited issues.`, - "FREE_TIER_ISSUE_LIMIT", - 403, - ); - } - } + // const user = await getUserById(req.userId); + // if (user && user.plan !== "pro") { + // const issueCount = await getOrganisationIssueCount(project.organisationId); + // if (issueCount >= FREE_TIER_LIMITS.issuesPerOrganisation) { + // return errorResponse( + // `free tier is limited to ${FREE_TIER_LIMITS.issuesPerOrganisation} issues per organisation. upgrade to pro for unlimited issues.`, + // "FREE_TIER_ISSUE_LIMIT", + // 403, + // ); + // } + // } const issue = await createIssue( project.id, diff --git a/packages/backend/src/routes/organisation/add-member.ts b/packages/backend/src/routes/organisation/add-member.ts index 1b06abc..dd3a987 100644 --- a/packages/backend/src/routes/organisation/add-member.ts +++ b/packages/backend/src/routes/organisation/add-member.ts @@ -2,10 +2,10 @@ import { OrgAddMemberRequestSchema } from "@sprint/shared"; import type { AuthedRequest } from "../../auth/middleware"; import { createOrganisationMember, - FREE_TIER_LIMITS, + // FREE_TIER_LIMITS, getOrganisationById, getOrganisationMemberRole, - getOrganisationMembers, + // getOrganisationMembers, getUserById, } from "../../db/queries"; import { updateSeatCount } from "../../lib/seats"; @@ -42,17 +42,17 @@ export default async function organisationAddMember(req: AuthedRequest) { } // check free tier member limit - const requester = await getUserById(req.userId); - if (requester && requester.plan !== "pro") { - const members = await getOrganisationMembers(organisationId); - if (members.length >= FREE_TIER_LIMITS.membersPerOrganisation) { - return errorResponse( - `free tier is limited to ${FREE_TIER_LIMITS.membersPerOrganisation} members per organisation. upgrade to pro for unlimited members.`, - "FREE_TIER_MEMBER_LIMIT", - 403, - ); - } - } + // const requester = await getUserById(req.userId); + // if (requester && requester.plan !== "pro") { + // const members = await getOrganisationMembers(organisationId); + // if (members.length >= FREE_TIER_LIMITS.membersPerOrganisation) { + // return errorResponse( + // `free tier is limited to ${FREE_TIER_LIMITS.membersPerOrganisation} members per organisation. upgrade to pro for unlimited members.`, + // "FREE_TIER_MEMBER_LIMIT", + // 403, + // ); + // } + // } const member = await createOrganisationMember(organisationId, userId, role); diff --git a/packages/backend/src/routes/organisation/create.ts b/packages/backend/src/routes/organisation/create.ts index 7fe40d6..909ed1d 100644 --- a/packages/backend/src/routes/organisation/create.ts +++ b/packages/backend/src/routes/organisation/create.ts @@ -2,10 +2,10 @@ import { OrgCreateRequestSchema } from "@sprint/shared"; import type { AuthedRequest } from "../../auth/middleware"; import { createOrganisationWithOwner, - FREE_TIER_LIMITS, + // FREE_TIER_LIMITS, getOrganisationBySlug, - getUserById, - getUserOrganisationCount, + // getUserById, + // getUserOrganisationCount, } from "../../db/queries"; import { errorResponse, parseJsonBody } from "../../validation"; @@ -21,17 +21,17 @@ export default async function organisationCreate(req: AuthedRequest) { } // check free tier limit - const user = await getUserById(req.userId); - if (user && user.plan !== "pro") { - const orgCount = await getUserOrganisationCount(req.userId); - if (orgCount >= FREE_TIER_LIMITS.organisationsPerUser) { - return errorResponse( - `free tier is limited to ${FREE_TIER_LIMITS.organisationsPerUser} organisation. upgrade to pro for unlimited organisations.`, - "FREE_TIER_ORG_LIMIT", - 403, - ); - } - } + // const user = await getUserById(req.userId); + // if (user && user.plan !== "pro") { + // const orgCount = await getUserOrganisationCount(req.userId); + // if (orgCount >= FREE_TIER_LIMITS.organisationsPerUser) { + // return errorResponse( + // `free tier is limited to ${FREE_TIER_LIMITS.organisationsPerUser} organisation. upgrade to pro for unlimited organisations.`, + // "FREE_TIER_ORG_LIMIT", + // 403, + // ); + // } + // } const organisation = await createOrganisationWithOwner(name, slug, req.userId, description); diff --git a/packages/backend/src/routes/organisation/member-time-tracking.ts b/packages/backend/src/routes/organisation/member-time-tracking.ts index 1bdbc5e..132967c 100644 --- a/packages/backend/src/routes/organisation/member-time-tracking.ts +++ b/packages/backend/src/routes/organisation/member-time-tracking.ts @@ -5,8 +5,8 @@ import { getOrganisationById, getOrganisationMemberRole, getOrganisationMemberTimedSessions, - getOrganisationOwner, - getUserById, + // getOrganisationOwner, + // getUserById, } from "../../db/queries"; import { errorResponse, parseQueryParams } from "../../validation"; @@ -40,9 +40,9 @@ export default async function organisationMemberTimeTracking(req: AuthedRequest) } // check if organisation owner has pro subscription - const owner = await getOrganisationOwner(organisationId); - const ownerUser = owner ? await getUserById(owner.userId) : null; - const isPro = ownerUser?.plan === "pro"; + // const owner = await getOrganisationOwner(organisationId); + // const ownerUser = owner ? await getUserById(owner.userId) : null; + // const isPro = ownerUser?.plan === "pro"; const sessions = await getOrganisationMemberTimedSessions(organisationId, fromDate); @@ -57,12 +57,12 @@ export default async function organisationMemberTimeTracking(req: AuthedRequest) issueId: session.issueId, issueNumber: session.issueNumber, projectKey: session.projectKey, - timestamps: isPro ? session.timestamps : [], - endedAt: isPro ? session.endedAt : null, - createdAt: isPro ? session.createdAt : null, - workTimeMs: isPro ? actualWorkTimeMs : 0, - breakTimeMs: isPro ? actualBreakTimeMs : 0, - isRunning: isPro ? session.endedAt === null && isTimerRunning(timestamps) : false, + timestamps: session.timestamps, + endedAt: session.endedAt, + createdAt: session.createdAt, + workTimeMs: actualWorkTimeMs, + breakTimeMs: actualBreakTimeMs, + isRunning: session.endedAt === null && isTimerRunning(timestamps), }; }); diff --git a/packages/backend/src/routes/project/create.ts b/packages/backend/src/routes/project/create.ts index 4fead4a..46ac772 100644 --- a/packages/backend/src/routes/project/create.ts +++ b/packages/backend/src/routes/project/create.ts @@ -2,9 +2,9 @@ import { ProjectCreateRequestSchema } from "@sprint/shared"; import type { AuthedRequest } from "../../auth/middleware"; import { createProject, - FREE_TIER_LIMITS, + // FREE_TIER_LIMITS, getOrganisationMemberRole, - getOrganisationProjectCount, + // getOrganisationProjectCount, getProjectByKey, getUserById, } from "../../db/queries"; @@ -30,18 +30,19 @@ export default async function projectCreate(req: AuthedRequest) { } // check free tier limit - const creator = await getUserById(req.userId); - if (creator && creator.plan !== "pro") { - const projectCount = await getOrganisationProjectCount(organisationId); - if (projectCount >= FREE_TIER_LIMITS.projectsPerOrganisation) { - return errorResponse( - `free tier is limited to ${FREE_TIER_LIMITS.projectsPerOrganisation} project per organisation. upgrade to pro for unlimited projects.`, - "FREE_TIER_PROJECT_LIMIT", - 403, - ); - } - } + // const creator = await getUserById(req.userId); + // if (creator && creator.plan !== "pro") { + // const projectCount = await getOrganisationProjectCount(organisationId); + // if (projectCount >= FREE_TIER_LIMITS.projectsPerOrganisation) { + // return errorResponse( + // `free tier is limited to ${FREE_TIER_LIMITS.projectsPerOrganisation} project per organisation. upgrade to pro for unlimited projects.`, + // "FREE_TIER_PROJECT_LIMIT", + // 403, + // ); + // } + // } + const creator = await getUserById(req.userId); if (!creator) { return errorResponse(`creator not found`, "CREATOR_NOT_FOUND", 404); } diff --git a/packages/backend/src/routes/sprint/create.ts b/packages/backend/src/routes/sprint/create.ts index 3a6291f..d7fc89d 100644 --- a/packages/backend/src/routes/sprint/create.ts +++ b/packages/backend/src/routes/sprint/create.ts @@ -2,11 +2,11 @@ import { SprintCreateRequestSchema } from "@sprint/shared"; import type { AuthedRequest } from "../../auth/middleware"; import { createSprint, - FREE_TIER_LIMITS, + // FREE_TIER_LIMITS, getOrganisationMemberRole, getProjectByID, - getProjectSprintCount, - getSubscriptionByUserId, + // getProjectSprintCount, + // getSubscriptionByUserId, hasOverlappingSprints, } from "../../db/queries"; import { errorResponse, parseJsonBody } from "../../validation"; @@ -32,18 +32,18 @@ export default async function sprintCreate(req: AuthedRequest) { } // check free tier sprint limit - const subscription = await getSubscriptionByUserId(req.userId); - const isPro = subscription?.status === "active"; - if (!isPro) { - const sprintCount = await getProjectSprintCount(projectId); - if (sprintCount >= FREE_TIER_LIMITS.sprintsPerProject) { - return errorResponse( - `Free tier limited to ${FREE_TIER_LIMITS.sprintsPerProject} sprints per project. Upgrade to Pro for unlimited sprints.`, - "SPRINT_LIMIT_REACHED", - 403, - ); - } - } + // const subscription = await getSubscriptionByUserId(req.userId); + // const isPro = subscription?.status === "active"; + // if (!isPro) { + // const sprintCount = await getProjectSprintCount(projectId); + // if (sprintCount >= FREE_TIER_LIMITS.sprintsPerProject) { + // return errorResponse( + // `Free tier limited to ${FREE_TIER_LIMITS.sprintsPerProject} sprints per project. Upgrade to Pro for unlimited sprints.`, + // "SPRINT_LIMIT_REACHED", + // 403, + // ); + // } + // } const start = new Date(startDate); const end = new Date(endDate); diff --git a/packages/backend/src/routes/user/update.ts b/packages/backend/src/routes/user/update.ts index c1c3ce4..dd23e96 100644 --- a/packages/backend/src/routes/user/update.ts +++ b/packages/backend/src/routes/user/update.ts @@ -1,7 +1,7 @@ import { UserUpdateRequestSchema } from "@sprint/shared"; import type { AuthedRequest } from "../../auth/middleware"; import { hashPassword } from "../../auth/utils"; -import { getSubscriptionByUserId, getUserById } from "../../db/queries"; +import { getUserById } from "../../db/queries"; import { errorResponse, parseJsonBody } from "../../validation"; export default async function update(req: AuthedRequest) { @@ -24,17 +24,17 @@ export default async function update(req: AuthedRequest) { } // block free users from changing icon preference - if (iconPreference !== undefined && iconPreference !== user.iconPreference) { - const subscription = await getSubscriptionByUserId(req.userId); - const isPro = subscription?.status === "active"; - if (!isPro) { - return errorResponse( - "icon style customization is only available on Pro. Upgrade to customize your icon style.", - "ICON_STYLE_PRO_ONLY", - 403, - ); - } - } + // if (iconPreference !== undefined && iconPreference !== user.iconPreference) { + // const subscription = await getSubscriptionByUserId(req.userId); + // const isPro = subscription?.status === "active"; + // if (!isPro) { + // return errorResponse( + // "icon style customization is only available on Pro. Upgrade to customize your icon style.", + // "ICON_STYLE_PRO_ONLY", + // 403, + // ); + // } + // } let passwordHash: string | undefined; if (password !== undefined) { diff --git a/packages/backend/src/routes/user/upload-avatar.ts b/packages/backend/src/routes/user/upload-avatar.ts index bdfe994..1490da9 100644 --- a/packages/backend/src/routes/user/upload-avatar.ts +++ b/packages/backend/src/routes/user/upload-avatar.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; import sharp from "sharp"; import type { AuthedRequest } from "../../auth/middleware"; -import { getSubscriptionByUserId } from "../../db/queries"; +// import { getSubscriptionByUserId } from "../../db/queries"; import { s3Client, s3Endpoint, s3PublicUrl } from "../../s3"; const MAX_FILE_SIZE = 5 * 1024 * 1024; @@ -42,21 +42,21 @@ export default async function uploadAvatar(req: AuthedRequest) { const inputBuffer = Buffer.from(await file.arrayBuffer()); // check if user is pro - const subscription = await getSubscriptionByUserId(req.userId); - const isPro = subscription?.status === "active"; + // const subscription = await getSubscriptionByUserId(req.userId); + // const isPro = subscription?.status === "active"; // block animated avatars for free users - if (!isPro && file.type === "image/gif") { - const animated = await isAnimatedGIF(inputBuffer); - if (animated) { - return new Response( - JSON.stringify({ - error: "Animated avatars are only available on Pro. Upgrade to upload animated avatars.", - }), - { status: 403, headers: { "Content-Type": "application/json" } }, - ); - } - } + // if (!isPro && file.type === "image/gif") { + // const animated = await isAnimatedGIF(inputBuffer); + // if (animated) { + // return new Response( + // JSON.stringify({ + // error: "Animated avatars are only available on Pro. Upgrade to upload animated avatars.", + // }), + // { status: 403, headers: { "Content-Type": "application/json" } }, + // ); + // } + // } const isGIF = file.type === "image/gif"; const outputExtension = isGIF ? "gif" : "png"; diff --git a/packages/frontend/src/components/account.tsx b/packages/frontend/src/components/account.tsx index a88e3cd..d6f8073 100644 --- a/packages/frontend/src/components/account.tsx +++ b/packages/frontend/src/components/account.tsx @@ -1,7 +1,7 @@ import type { IconStyle } from "@sprint/shared"; import type { ReactNode } from "react"; import { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; +// import { Link } from "react-router-dom"; import { toast } from "sonner"; import { useAuthenticatedSession } from "@/components/session-provider"; import ThemeToggle from "@/components/theme-toggle"; @@ -38,11 +38,7 @@ function Account({ trigger }: { trigger?: ReactNode }) { setName(currentUser.name); setUsername(currentUser.username); setAvatarUrl(currentUser.avatarURL || null); - // free users are locked to pixel icon style - const effectiveIconStyle = - currentUser.plan === "pro" - ? ((currentUser.iconPreference as IconStyle) ?? DEFAULT_ICON_STYLE) - : DEFAULT_ICON_STYLE; + const effectiveIconStyle = (currentUser.iconPreference as IconStyle) ?? DEFAULT_ICON_STYLE; setIconPreference(effectiveIconStyle); setPassword(""); @@ -59,13 +55,11 @@ function Account({ trigger }: { trigger?: ReactNode }) { } try { - // only send iconPreference for pro users - const effectiveIconPreference = currentUser.plan === "pro" ? iconPreference : undefined; const data = await updateUser.mutateAsync({ name: name.trim(), password: password.trim() || undefined, avatarURL, - iconPreference: effectiveIconPreference, + iconPreference, }); setError(""); setUser(data); @@ -141,22 +135,9 @@ function Account({ trigger }: { trigger?: ReactNode }) {
- - setIconPreference(v as IconStyle)}> + @@ -180,21 +161,21 @@ function Account({ trigger }: { trigger?: ReactNode }) { - {currentUser.plan !== "pro" && ( + {/* {currentUser.plan !== "pro" && ( Upgrade to Pro {" "} to customize icon style - )} + )} */}
{error !== "" && } - {/* Show subscription management link */} -
+ {/* subscription management link commented out for beta */} + {/*
{currentUser.plan === "pro" ? ( )} -
+
*/}
@@ -168,7 +162,7 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) { Create Issue - {!isPro && selectedProject && ( + {/* {!isPro && selectedProject && (
- )} + )} */}
@@ -301,16 +295,10 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) { type="submit" disabled={ submitting || - isAtIssueLimit || ((title.trim() === "" || title.trim().length > ISSUE_TITLE_MAX_LENGTH) && submitAttempted) || (description.trim().length > ISSUE_DESCRIPTION_MAX_LENGTH && submitAttempted) } - title={ - isAtIssueLimit - ? "Free tier limited to 100 issues per organisation. Upgrade to Pro for unlimited." - : undefined - } > {submitting ? "Creating..." : "Create"} diff --git a/packages/frontend/src/components/organisation-select.tsx b/packages/frontend/src/components/organisation-select.tsx index 2253cfb..5a52d93 100644 --- a/packages/frontend/src/components/organisation-select.tsx +++ b/packages/frontend/src/components/organisation-select.tsx @@ -1,9 +1,9 @@ import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; -import { FreeTierLimit } from "@/components/free-tier-limit"; +// import { FreeTierLimit } from "@/components/free-tier-limit"; import { OrganisationForm } from "@/components/organisation-form"; import { useSelection } from "@/components/selection-provider"; -import { useAuthenticatedSession } from "@/components/session-provider"; +// import { useAuthenticatedSession } from "@/components/session-provider"; import { Button } from "@/components/ui/button"; import { Select, @@ -19,7 +19,7 @@ import { useOrganisations } from "@/lib/query/hooks"; import { cn } from "@/lib/utils"; import OrgIcon from "./org-icon"; -const FREE_TIER_ORG_LIMIT = 1; +// const free_tier_org_limit = 1; export function OrganisationSelect({ placeholder = "Select Organisation", @@ -44,11 +44,10 @@ export function OrganisationSelect({ const [pendingOrganisationId, setPendingOrganisationId] = useState(null); const { data: organisationsData = [] } = useOrganisations(); const { selectedOrganisationId, selectOrganisation } = useSelection(); - const { user } = useAuthenticatedSession(); - - const isPro = user.plan === "pro"; - const orgCount = organisationsData.length; - const isAtOrgLimit = !isPro && orgCount >= FREE_TIER_ORG_LIMIT; + // const { user } = useAuthenticatedSession(); + // const isPro = user.plan === "pro"; + // const orgCount = organisationsData.length; + // const isAtOrgLimit = !isPro && orgCount >= FREE_TIER_ORG_LIMIT; const organisations = useMemo( () => [...organisationsData].sort((a, b) => a.Organisation.name.localeCompare(b.Organisation.name)), @@ -116,7 +115,7 @@ export function OrganisationSelect({ {organisations.length > 0 && } - {!isPro && ( + {/* {!isPro && (
- )} + )} */} + } diff --git a/packages/frontend/src/components/organisations.tsx b/packages/frontend/src/components/organisations.tsx index 0133903..36fb2ba 100644 --- a/packages/frontend/src/components/organisations.tsx +++ b/packages/frontend/src/components/organisations.tsx @@ -8,10 +8,10 @@ import { } from "@sprint/shared"; import { useQueryClient } from "@tanstack/react-query"; import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; -import { Link } from "react-router-dom"; +// import { Link } from "react-router-dom"; import { toast } from "sonner"; import { AddMember } from "@/components/add-member"; -import { FreeTierLimit } from "@/components/free-tier-limit"; +// import { FreeTierLimit } from "@/components/free-tier-limit"; import OrgIcon from "@/components/org-icon"; import { OrganisationForm } from "@/components/organisation-form"; import { OrganisationSelect } from "@/components/organisation-select"; @@ -44,7 +44,7 @@ import { useDeleteOrganisation, useDeleteProject, useDeleteSprint, - useIssues, + // useIssues, useOrganisationMembers, useOrganisationMemberTimeTracking, useOrganisations, @@ -58,15 +58,15 @@ import { } from "@/lib/query/hooks"; import { queryKeys } from "@/lib/query/keys"; import { apiClient } from "@/lib/server"; -import { capitalise, cn, formatDuration, unCamelCase } from "@/lib/utils"; +import { capitalise, formatDuration, unCamelCase } from "@/lib/utils"; import { Switch } from "./ui/switch"; -const FREE_TIER_LIMITS = { - organisationsPerUser: 1, - projectsPerOrganisation: 1, - issuesPerOrganisation: 100, - membersPerOrganisation: 5, -} as const; +// const FREE_TIER_LIMITS = { +// organisationsPerUser: 1, +// projectsPerOrganisation: 1, +// issuesPerOrganisation: 100, +// membersPerOrganisation: 5, +// } as const; function Organisations({ trigger }: { trigger?: ReactNode }) { const { user } = useAuthenticatedSession(); @@ -76,7 +76,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { const { data: projectsData = [] } = useProjects(selectedOrganisationId); const { data: sprints = [] } = useSprints(selectedProjectId); const { data: membersData = [] } = useOrganisationMembers(selectedOrganisationId); - const { data: issues = [] } = useIssues(selectedProjectId); + // const { data: issues = [] } = useIssues(selectedProjectId); const updateOrganisation = useUpdateOrganisation(); const updateMemberRole = useUpdateOrganisationMemberRole(); const removeMember = useRemoveOrganisationMember(); @@ -86,11 +86,11 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { const replaceIssueStatus = useReplaceIssueStatus(); const replaceIssueType = useReplaceIssueType(); - const isPro = user.plan === "pro"; - const orgCount = organisationsData.length; - const projectCount = projectsData.length; - const issueCount = issues.length; - const memberCount = membersData.length; + // const isPro = user.plan === "pro"; + // const orgCount = organisationsData.length; + // const projectCount = projectsData.length; + // const issueCount = issues.length; + // const memberCount = membersData.length; const organisations = useMemo( () => [...organisationsData].sort((a, b) => a.Organisation.name.localeCompare(b.Organisation.name)), @@ -842,7 +842,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
{/* Free tier limits section */} - {!isPro && ( + {/* {!isPro && (

Plan Limits

@@ -881,7 +881,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { />
- )} + )} */} {isAdmin && (
@@ -943,40 +943,36 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { {isAdmin && (
- {isPro && ( - <> - - - - - - date && setFromDate(date)} - autoFocus - /> - - - - - - - - downloadTimeTrackingData("csv")}> - Download CSV - - downloadTimeTrackingData("json")}> - Download JSON - - - - - )} + + + + + + date && setFromDate(date)} + autoFocus + /> + + + + + + + + downloadTimeTrackingData("csv")}> + Download CSV + + downloadTimeTrackingData("json")}> + Download JSON + + +
)}
@@ -994,7 +990,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
- {isAdmin && isPro && ( + {isAdmin && ( {formatDuration(member.totalTimeMs)} @@ -1033,7 +1029,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
{isAdmin && ( <> - {!isPro && ( + {/* {!isPro && (
= FREE_TIER_LIMITS.membersPerOrganisation} />
- )} + )} */} m.User.username)} @@ -1058,15 +1054,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { void invalidateMembers(); }} trigger={ - } @@ -1522,14 +1510,14 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {

Features

- {!isPro && ( + {/* {!isPro && (
Feature toggling is only available on Pro.{" "} Upgrade to customize features.
- )} + )} */}
{Object.keys(DEFAULT_FEATURES).map((feature) => (
@@ -1551,12 +1539,9 @@ function Organisations({ trigger }: { trigger?: ReactNode }) { ); await invalidateOrganisations(); }} - disabled={!isPro} color={"#ff0000"} /> - - {unCamelCase(feature)} - + {unCamelCase(feature)}
))}
diff --git a/packages/frontend/src/components/project-select.tsx b/packages/frontend/src/components/project-select.tsx index 618dde7..28e6018 100644 --- a/packages/frontend/src/components/project-select.tsx +++ b/packages/frontend/src/components/project-select.tsx @@ -1,8 +1,8 @@ import { useEffect, useMemo, useState } from "react"; -import { FreeTierLimit } from "@/components/free-tier-limit"; +// import { FreeTierLimit } from "@/components/free-tier-limit"; import { ProjectForm } from "@/components/project-form"; import { useSelection } from "@/components/selection-provider"; -import { useAuthenticatedSession } from "@/components/session-provider"; +// import { useAuthenticatedSession } from "@/components/session-provider"; import { Button } from "@/components/ui/button"; import { Select, @@ -16,7 +16,7 @@ import { } from "@/components/ui/select"; import { useProjects } from "@/lib/query/hooks"; -const FREE_TIER_PROJECT_LIMIT = 1; +// const free_tier_project_limit = 1; export function ProjectSelect({ placeholder = "Select Project", @@ -33,11 +33,10 @@ export function ProjectSelect({ const [pendingProjectId, setPendingProjectId] = useState(null); const { selectedOrganisationId, selectedProjectId, selectProject } = useSelection(); const { data: projectsData = [] } = useProjects(selectedOrganisationId); - const { user } = useAuthenticatedSession(); - - const isPro = user.plan === "pro"; - const projectCount = projectsData.length; - const isAtProjectLimit = !isPro && projectCount >= FREE_TIER_PROJECT_LIMIT; + // const { user } = useAuthenticatedSession(); + // const isPro = user.plan === "pro"; + // const projectCount = projectsData.length; + // const isAtProjectLimit = !isPro && projectCount >= FREE_TIER_PROJECT_LIMIT; const projects = useMemo( () => [...projectsData].sort((a, b) => a.Project.name.localeCompare(b.Project.name)), @@ -91,7 +90,7 @@ export function ProjectSelect({ {projects.length > 0 && } - {!isPro && selectedOrganisationId && ( + {/* {!isPro && selectedOrganisationId && (
- )} + )} */} Create Project diff --git a/packages/frontend/src/components/sprint-form.tsx b/packages/frontend/src/components/sprint-form.tsx index d550879..7e67606 100644 --- a/packages/frontend/src/components/sprint-form.tsx +++ b/packages/frontend/src/components/sprint-form.tsx @@ -1,7 +1,7 @@ import { DEFAULT_SPRINT_COLOUR, type SprintRecord } from "@sprint/shared"; import { type FormEvent, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; -import { FreeTierLimit } from "@/components/free-tier-limit"; +// import { FreeTierLimit } from "@/components/free-tier-limit"; import { useAuthenticatedSession } from "@/components/session-provider"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; @@ -22,7 +22,7 @@ import { parseError } from "@/lib/server"; import { cn } from "@/lib/utils"; const SPRINT_NAME_MAX_LENGTH = 64; -const FREE_TIER_SPRINT_LIMIT = 5; +// const free_tier_sprint_limit = 5; const getStartOfDay = (date: Date) => { const next = new Date(date); @@ -303,7 +303,7 @@ export function SprintForm({ )}
- {!isEdit && ( + {/* {!isEdit && ( = FREE_TIER_SPRINT_LIMIT} /> - )} + )} */}
@@ -324,13 +324,7 @@ export function SprintForm({ disabled={ submitting || ((name.trim() === "" || name.trim().length > SPRINT_NAME_MAX_LENGTH) && submitAttempted) || - (dateError !== "" && submitAttempted) || - (!isEdit && user.plan !== "pro" && sprints.length >= FREE_TIER_SPRINT_LIMIT) - } - title={ - !isEdit && user.plan !== "pro" && sprints.length >= FREE_TIER_SPRINT_LIMIT - ? `Free tier limited to ${FREE_TIER_SPRINT_LIMIT} sprints per project. Upgrade to Pro for unlimited sprints.` - : undefined + (dateError !== "" && submitAttempted) } > {submitting ? (isEdit ? "Saving..." : "Creating...") : isEdit ? "Save" : "Create"} diff --git a/packages/frontend/src/components/top-bar.tsx b/packages/frontend/src/components/top-bar.tsx index a47e8e3..9f78398 100644 --- a/packages/frontend/src/components/top-bar.tsx +++ b/packages/frontend/src/components/top-bar.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo } from "react"; -import { Link, useLocation, useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import Account from "@/components/account"; import { IssueForm } from "@/components/issue-form"; import LogOutButton from "@/components/log-out-button"; @@ -11,7 +11,7 @@ import { useSelection } from "@/components/selection-provider"; import { useAuthenticatedSession } from "@/components/session-provider"; import SmallUserDisplay from "@/components/small-user-display"; import { SprintForm } from "@/components/sprint-form"; -import { Button } from "@/components/ui/button"; +// import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, @@ -123,11 +123,11 @@ export default function TopBar({ showIssueForm = true }: { showIssueForm?: boole )}
- {user.plan !== "pro" && ( + {/* {user.plan !== "pro" && ( - )} + )} */} diff --git a/packages/frontend/src/components/upload-avatar.tsx b/packages/frontend/src/components/upload-avatar.tsx index 754a6fc..d5b6379 100644 --- a/packages/frontend/src/components/upload-avatar.tsx +++ b/packages/frontend/src/components/upload-avatar.tsx @@ -1,7 +1,7 @@ import { useRef, useState } from "react"; import { toast } from "sonner"; import Avatar from "@/components/avatar"; -import { useSession } from "@/components/session-provider"; +// import { useSession } from "@/components/session-provider"; import { Button } from "@/components/ui/button"; import Icon from "@/components/ui/icon"; import { Label } from "@/components/ui/label"; @@ -9,36 +9,36 @@ import { useUploadAvatar } from "@/lib/query/hooks"; import { parseError } from "@/lib/server"; import { cn } from "@/lib/utils"; -function isAnimatedGIF(file: File): Promise { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = () => { - const buffer = reader.result as ArrayBuffer; - const arr = new Uint8Array(buffer); - // check for GIF89a or GIF87a header - const header = String.fromCharCode(...arr.slice(0, 6)); - if (header !== "GIF89a" && header !== "GIF87a") { - resolve(false); - return; - } - // look for multiple images (animation indicator) - // GIFs have image descriptors starting with 0x2C - // and graphic control extensions starting with 0x21 0xF9 - let frameCount = 0; - let i = 6; // skip header - while (i < arr.length - 1) { - if (arr[i] === 0x21 && arr[i + 1] === 0xf9) { - // graphic control extension - indicates animation frame - frameCount++; - } - i++; - } - resolve(frameCount > 1); - }; - reader.onerror = () => resolve(false); - reader.readAsArrayBuffer(file.slice(0, 1024)); // only need first 1KB for header check - }); -} +// function isAnimatedGIF(file: File): Promise { +// return new Promise((resolve) => { +// const reader = new FileReader(); +// reader.onload = () => { +// const buffer = reader.result as ArrayBuffer; +// const arr = new Uint8Array(buffer); +// // check for GIF89a or GIF87a header +// const header = String.fromCharCode(...arr.slice(0, 6)); +// if (header !== "GIF89a" && header !== "GIF87a") { +// resolve(false); +// return; +// } +// // look for multiple images (animation indicator) +// // GIFs have image descriptors starting with 0x2C +// // and graphic control extensions starting with 0x21 0xF9 +// let frameCount = 0; +// let i = 6; // skip header +// while (i < arr.length - 1) { +// if (arr[i] === 0x21 && arr[i + 1] === 0xf9) { +// // graphic control extension - indicates animation frame +// frameCount++; +// } +// i++; +// } +// resolve(frameCount > 1); +// }; +// reader.onerror = () => resolve(false); +// reader.readAsArrayBuffer(file.slice(0, 1024)); // only need first 1KB for header check +// }); +// } export function UploadAvatar({ name, @@ -56,7 +56,7 @@ export function UploadAvatar({ skipOrgCheck?: boolean; className?: string; }) { - const { user } = useSession(); + // const { user } = useSession(); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const fileInputRef = useRef(null); @@ -68,20 +68,20 @@ export function UploadAvatar({ if (!file) return; // check for animated GIF for free users - if (user?.plan !== "pro" && file.type === "image/gif") { - const isAnimated = await isAnimatedGIF(file); - if (isAnimated) { - setError("Animated avatars are only available on Pro. Upgrade to upload animated avatars."); - toast.error("Animated avatars are only available on Pro. Upgrade to upload animated avatars.", { - dismissible: false, - }); - // reset file input - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - return; - } - } + // if (user?.plan !== "pro" && file.type === "image/gif") { + // const isAnimated = await isAnimatedGIF(file); + // if (isAnimated) { + // setError("Animated avatars are only available on Pro. Upgrade to upload animated avatars."); + // toast.error("Animated avatars are only available on Pro. Upgrade to upload animated avatars.", { + // dismissible: false, + // }); + // // reset file input + // if (fileInputRef.current) { + // fileInputRef.current.value = ""; + // } + // return; + // } + // } setUploading(true); setError(null); @@ -99,25 +99,9 @@ export function UploadAvatar({ setError(message); setUploading(false); - // check if the error is about animated avatars for free users - if (message.toLowerCase().includes("animated") && message.toLowerCase().includes("pro")) { - toast.error( -
- Animated avatars are only available on Pro. - - Upgrade to Pro - -
, - { - dismissible: false, - duration: 5000, - }, - ); - } else { - toast.error(`Error uploading avatar: ${message}`, { - dismissible: false, - }); - } + toast.error(`Error uploading avatar: ${message}`, { + dismissible: false, + }); } }; diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx index 48e3cac..9e2e040 100644 --- a/packages/frontend/src/main.tsx +++ b/packages/frontend/src/main.tsx @@ -13,7 +13,7 @@ import Font from "@/pages/Font"; import Issues from "@/pages/Issues"; import Landing from "@/pages/Landing"; import NotFound from "@/pages/NotFound"; -import Plans from "@/pages/Plans"; +// import plans from "@/pages/Plans"; import Test from "@/pages/Test"; import Timeline from "@/pages/Timeline"; @@ -31,14 +31,14 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( } /> {/* authed routes */} - } - /> + /> */} Your account: You're responsible for keeping your login details secure. Don't share your account.

-

+ {/*

Payments: Pro plans are billed monthly or annually. Cancel anytime from your account settings. No refunds for partial months. -

+

*/}

Service availability: We aim for 99.9% uptime but can't guarantee it. We may occasionally need downtime for maintenance. diff --git a/packages/frontend/src/pages/Landing.tsx b/packages/frontend/src/pages/Landing.tsx index 7af676b..6fcb824 100644 --- a/packages/frontend/src/pages/Landing.tsx +++ b/packages/frontend/src/pages/Landing.tsx @@ -6,26 +6,26 @@ import ThemeToggle from "@/components/theme-toggle"; import { Button } from "@/components/ui/button"; import Icon from "@/components/ui/icon"; -const faqs = [ - { - question: "What payment methods do you accept?", - answer: "We accept all major credit cards.", - }, - { - question: "What if I need more users?", - answer: - "Pro plan pricing scales with your team. Add or remove users anytime, and we'll adjust your billing automatically.", - }, - { - question: "Can I cancel anytime?", - answer: - "Absolutely. Cancel anytime with no questions asked. You'll keep access until the end of your billing period.", - }, - { - question: "Do you offer refunds?", - answer: "Yes, we offer a 30-day money-back guarantee. If Sprint isn't right for you, just let us know.", - }, -]; +// const faqs = [ +// { +// question: "What payment methods do you accept?", +// answer: "We accept all major credit cards.", +// }, +// { +// question: "What if I need more users?", +// answer: +// "Pro plan pricing scales with your team. Add or remove users anytime, and we'll adjust your billing automatically.", +// }, +// { +// question: "Can I cancel anytime?", +// answer: +// "Absolutely. Cancel anytime with no questions asked. You'll keep access until the end of your billing period.", +// }, +// { +// question: "Do you offer refunds?", +// answer: "Yes, we offer a 30-day money-back guarantee. If Sprint isn't right for you, just let us know.", +// }, +// ]; export default function Landing() { const { user, isLoading } = useSession(); @@ -52,12 +52,12 @@ export default function Landing() { > Features - Pricing - + */} setLoginModalOpen(true)}> Get started - + */} )}

-

Free forever · Upgrade when you need more

+ {/*

Free forever · Upgrade when you need more

*/} {/* problem section */} @@ -217,7 +217,7 @@ export default function Landing() { {/* pricing section */} -
+ {/*

Simple, transparent pricing

@@ -227,10 +227,10 @@ export default function Landing() { View plans

-
+
*/} {/* faq section */} -
+ {/*

Frequently Asked Questions

@@ -242,7 +242,7 @@ export default function Landing() { ))}
-
+
*/} {/* TODO:> commented out until we have actual testimonies */} {/* social proof placeholder */} @@ -287,9 +287,9 @@ export default function Landing() { )} -

+ {/*

Free forever · Upgrade when you need more · Cancel anytime -

+

*/} diff --git a/packages/frontend/src/pages/Plans.tsx b/packages/frontend/src/pages/Plans.tsx index 04dba89..8b5938b 100644 --- a/packages/frontend/src/pages/Plans.tsx +++ b/packages/frontend/src/pages/Plans.tsx @@ -1,126 +1,125 @@ -import { format } from "date-fns"; -import { useMemo, useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; +// import { format } from "date-fns"; +import { useState } from "react"; +import { Link } from "react-router-dom"; import { LoginModal } from "@/components/login-modal"; -import { PricingCard, pricingTiers } from "@/components/pricing-card"; +// import { PricingCard, pricingTiers } from "@/components/pricing-card"; import { useSession } from "@/components/session-provider"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; +// import { +// AlertDialog, +// AlertDialogAction, +// AlertDialogCancel, +// AlertDialogContent, +// AlertDialogDescription, +// AlertDialogFooter, +// AlertDialogHeader, +// AlertDialogTitle, +// AlertDialogTrigger, +// } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; -import Icon from "@/components/ui/icon"; -import { Switch } from "@/components/ui/switch"; -import { - useCancelSubscription, - useCreateCheckoutSession, - useCreatePortalSession, - useSubscription, -} from "@/lib/query/hooks"; -import { cn } from "@/lib/utils"; +// import Icon from "@/components/ui/icon"; +// import { Switch } from "@/components/ui/switch"; +// import { +// useCancelSubscription, +// useCreateCheckoutSession, +// useCreatePortalSession, +// useSubscription, +// } from "@/lib/query/hooks"; +// import { cn } from "@/lib/utils"; export default function Plans() { const { user, isLoading } = useSession(); - const navigate = useNavigate(); - const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("annual"); const [loginModalOpen, setLoginModalOpen] = useState(false); - const [processingTier, setProcessingTier] = useState(null); + // const navigate = useNavigate(); + // const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("annual"); + // const [processingTier, setProcessingTier] = useState(null); - const { data: subscriptionData } = useSubscription(); - const createCheckoutSession = useCreateCheckoutSession(); - const createPortalSession = useCreatePortalSession(); - const cancelSubscription = useCancelSubscription(); + // const { data: subscriptionData } = useSubscription(); + // const createCheckoutSession = useCreateCheckoutSession(); + // const createPortalSession = useCreatePortalSession(); + // const cancelSubscription = useCancelSubscription(); - const subscription = subscriptionData?.subscription ?? null; - const isProUser = - user?.plan === "pro" || subscription?.status === "active" || subscription?.status === "trialing"; - const isCancellationScheduled = Boolean(subscription?.cancelAtPeriodEnd); - const isCanceled = subscription?.status === "canceled"; - const cancellationEndDate = useMemo(() => { - if (!subscription?.currentPeriodEnd) return null; - const date = new Date(subscription.currentPeriodEnd); - if (Number.isNaN(date.getTime())) return null; - return format(date, "d MMM yyyy"); - }, [subscription?.currentPeriodEnd]); - const [cancelDialogOpen, setCancelDialogOpen] = useState(false); - const [cancelError, setCancelError] = useState(null); + // const subscription = subscriptionData?.subscription ?? null; + // const isProUser = + // user?.plan === "pro" || subscription?.status === "active" || subscription?.status === "trialing"; + // const isCancellationScheduled = Boolean(subscription?.cancelAtPeriodEnd); + // const isCanceled = subscription?.status === "canceled"; + // const cancellationEndDate = useMemo(() => { + // if (!subscription?.currentPeriodEnd) return null; + // const date = new Date(subscription.currentPeriodEnd); + // if (Number.isNaN(date.getTime())) return null; + // return format(date, "d MMM yyyy"); + // }, [subscription?.currentPeriodEnd]); + // const [cancelDialogOpen, setCancelDialogOpen] = useState(false); + // const [cancelError, setCancelError] = useState(null); - const handleTierAction = async (tierName: string) => { - if (!user) { - setLoginModalOpen(true); - return; - } + // const handleTierAction = async (tierName: string) => { + // if (!user) { + // setLoginModalOpen(true); + // return; + // } + // + // if (tierName === "Pro") { + // if (isProUser) { + // // open customer portal + // setProcessingTier(tierName); + // try { + // const result = await createPortalSession.mutateAsync(); + // if (result.url) { + // window.location.href = result.url; + // } else { + // setProcessingTier(null); + // } + // } catch { + // setProcessingTier(null); + // } + // } else { + // // start checkout + // setProcessingTier(tierName); + // try { + // const result = await createCheckoutSession.mutateAsync({ billingPeriod }); + // if (result.url) { + // window.location.href = result.url; + // } else { + // setProcessingTier(null); + // } + // } catch { + // setProcessingTier(null); + // } + // } + // } + // // starter tier - just go to issues if not already there + // if (tierName === "Starter") { + // navigate("/issues"); + // } + // }; - if (tierName === "Pro") { - if (isProUser) { - // open customer portal - setProcessingTier(tierName); - try { - const result = await createPortalSession.mutateAsync(); - if (result.url) { - window.location.href = result.url; - } else { - setProcessingTier(null); - } - } catch { - setProcessingTier(null); - } - } else { - // start checkout - setProcessingTier(tierName); - try { - const result = await createCheckoutSession.mutateAsync({ billingPeriod }); - if (result.url) { - window.location.href = result.url; - } else { - setProcessingTier(null); - } - } catch { - setProcessingTier(null); - } - } - } - // starter tier - just go to issues if not already there - if (tierName === "Starter") { - navigate("/issues"); - } - }; + // const handleCancelSubscription = async () => { + // setCancelError(null); + // try { + // await cancelSubscription.mutateAsync(); + // setCancelDialogOpen(false); + // } catch (error) { + // const message = error instanceof Error ? error.message : "failed to cancel subscription"; + // setCancelError(message); + // } + // }; - const handleCancelSubscription = async () => { - setCancelError(null); - try { - await cancelSubscription.mutateAsync(); - setCancelDialogOpen(false); - } catch (error) { - const message = error instanceof Error ? error.message : "failed to cancel subscription"; - setCancelError(message); - } - }; - - // modify pricing tiers based on user's current plan - const modifiedTiers = pricingTiers.map((tier) => { - const isCurrentPlan = tier.name === "Pro" && isProUser; - const isStarterCurrent = tier.name === "Starter" && !!user && !isProUser; - - return { - ...tier, - highlighted: isCurrentPlan || (!isProUser && tier.name === "Pro"), - cta: isCurrentPlan - ? "Manage subscription" - : isStarterCurrent - ? "Current plan" - : tier.name === "Pro" - ? "Upgrade to Pro" - : tier.cta, - }; - }); + // const modifiedTiers = pricingTiers.map((tier) => { + // const isCurrentPlan = tier.name === "Pro" && isProUser; + // const isStarterCurrent = tier.name === "Starter" && !!user && !isProUser; + // + // return { + // ...tier, + // highlighted: isCurrentPlan || (!isProUser && tier.name === "Pro"), + // cta: isCurrentPlan + // ? "Manage subscription" + // : isStarterCurrent + // ? "Current plan" + // : tier.name === "Pro" + // ? "Upgrade to Pro" + // : tier.cta, + // }; + // }); return (
@@ -153,149 +152,7 @@ export default function Plans() {
-
-
-

- {user ? "Choose your plan" : "Simple, transparent pricing"} -

-

- {user - ? isProUser - ? "You are currently on the Pro plan. Manage your subscription or switch plans below." - : "You are currently on the Starter plan. Upgrade to Pro for unlimited access." - : "Choose the plan that fits your team. Scale as you grow."} -

- - {/* billing toggle */} -
- - setBillingPeriod(checked ? "annual" : "monthly")} - className="bg-border data-[state=checked]:bg-border! data-[state=unchecked]:bg-border!" - thumbClassName="bg-personality dark:bg-personality data-[state=checked]:bg-personality! data-[state=unchecked]:bg-personality!" - aria-label="toggle billing period" - /> - - - Save 17% - -
-
- -
- {modifiedTiers.map((tier) => ( - handleTierAction(tier.name)} - disabled={processingTier !== null || tier.name === "Starter"} - loading={processingTier === tier.name} - /> - ))} -
- - {user && isProUser && ( -
-
-
-

Cancel subscription

-

- {isCancellationScheduled || isCanceled - ? `Cancelled, benefits end on ${cancellationEndDate ?? "your billing end date"}.` - : "Canceling will keep access until the end of your billing period."} -

-
- { - setCancelDialogOpen(open); - if (!open) setCancelError(null); - }} - > - - - - - - Cancel subscription? - - You will keep Pro access until the end of your current billing period. - - - - Keep subscription - - {cancelSubscription.isPending ? "Canceling..." : "Confirm cancel"} - - - {cancelError &&

{cancelError}

} -
-
-
-
- )} - - {/* trust signals */} -
-
- -

Secure & Encrypted

-

Your data is safe with us

-
-
- -

Free Starter Plan

-

Get started instantly

-
-
- -

Money Back Guarantee

-

30-day no-risk policy

-
-
- -
- - The boring stuff — Privacy Policy & ToS - -
-
+ {/* pricing content commented out for beta */}