From 953a6978d13fb7ccc3bfc7bb880237a0347bc859 Mon Sep 17 00:00:00 2001
From: Oliver Bryan
Date: Thu, 29 Jan 2026 12:09:13 +0000
Subject: [PATCH 1/4] Update bun.lock
---
bun.lock | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/bun.lock b/bun.lock
index e26f82d..ecb03a0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -61,10 +61,10 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@sprint/shared": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
- "@tanstack/react-query": "^5.90.19",
+ "@tanstack/react-query": "^5.90.20",
"@tanstack/react-query-devtools": "^5.91.2",
- "@tauri-apps/api": "^2",
- "@tauri-apps/plugin-opener": "^2",
+ "@tauri-apps/api": "^2.9.1",
+ "@tauri-apps/plugin-opener": "^2.5.3",
"@ts-rest/core": "^3.52.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -76,21 +76,21 @@
"react-colorful": "^5.6.1",
"react-day-picker": "^9.13.0",
"react-dom": "19.2.4",
- "react-resizable-panels": "^4.0.15",
- "react-router-dom": "^7.10.1",
+ "react-resizable-panels": "^4.5.3",
+ "react-router-dom": "^7.13.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
},
"devDependencies": {
- "@tauri-apps/cli": "^2",
- "@types/node": "^25.0.1",
- "@types/react": "^19.1.8",
- "@types/react-dom": "^19.1.6",
- "@vitejs/plugin-react": "^4.6.0",
+ "@tauri-apps/cli": "^2.9.6",
+ "@types/node": "^25.1.0",
+ "@types/react": "^19.2.10",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^4.7.0",
"tw-animate-css": "^1.4.0",
"typescript": "~5.8.3",
- "vite": "^7.0.4",
+ "vite": "^7.3.1",
},
},
"packages/shared": {
From 07353a5d9f58caf83487d4096fa136607ae4cf92 Mon Sep 17 00:00:00 2001
From: Oliver Bryan
Date: Thu, 29 Jan 2026 12:09:28 +0000
Subject: [PATCH 2/4] Changed pricing section on Landing to simple /plans link
---
packages/frontend/src/pages/Landing.tsx | 106 ++++--------------------
1 file changed, 16 insertions(+), 90 deletions(-)
diff --git a/packages/frontend/src/pages/Landing.tsx b/packages/frontend/src/pages/Landing.tsx
index bab3e2e..7af676b 100644
--- a/packages/frontend/src/pages/Landing.tsx
+++ b/packages/frontend/src/pages/Landing.tsx
@@ -1,13 +1,10 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { LoginModal } from "@/components/login-modal";
-import { PricingCard, pricingTiers } from "@/components/pricing-card";
import { useSession } from "@/components/session-provider";
import ThemeToggle from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import Icon from "@/components/ui/icon";
-import { Switch } from "@/components/ui/switch";
-import { cn } from "@/lib/utils";
const faqs = [
{
@@ -32,7 +29,6 @@ const faqs = [
export default function Landing() {
const { user, isLoading } = useSession();
- const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("annual");
const [loginModalOpen, setLoginModalOpen] = useState(false);
return (
@@ -221,99 +217,29 @@ export default function Landing() {
{/* pricing section */}
-
+
Simple, transparent pricing
Choose the plan that fits your team. Scale as you grow.
-
- {/* billing toggle */}
-
- setBillingPeriod("monthly")}
- className={cn(
- "text-lg transition-colors",
- billingPeriod === "monthly" ? "text-foreground font-700" : "text-muted-foreground",
- )}
- >
- monthly
-
- 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"
- />
- setBillingPeriod("annual")}
- className={cn(
- "text-lg transition-colors",
- billingPeriod === "annual" ? "text-foreground font-700" : "text-muted-foreground",
- )}
- >
- annual
-
-
- Save 17%
-
-
+
+ View plans
+
+
-
- {pricingTiers.map((tier) => (
-
setLoginModalOpen(true)}
- />
- ))}
-
-
- {/* trust signals */}
-
-
-
-
Secure & Encrypted
-
Your data is safe with us
-
-
-
-
Free Starter Plan
-
Get started instantly
-
-
-
-
Money Back Guarantee
-
30-day no-risk policy
-
-
-
- {/* faq section */}
-
-
-
Frequently Asked Questions
-
- {faqs.map((faq) => (
-
-
{faq.question}
-
{faq.answer}
-
- ))}
-
+ {/* faq section */}
+
+
+
Frequently Asked Questions
+
+ {faqs.map((faq) => (
+
+
{faq.question}
+
{faq.answer}
+
+ ))}
From 232e00a2b0c1017e6605d42ae590cb1801b6c4be Mon Sep 17 00:00:00 2001
From: Oliver Bryan
Date: Thu, 29 Jan 2026 12:11:52 +0000
Subject: [PATCH 3/4] Update todo.md
---
todo.md | 29 +++++++++++++++++++----------
1 file changed, 19 insertions(+), 10 deletions(-)
diff --git a/todo.md b/todo.md
index 4b511e9..7efa4e8 100644
--- a/todo.md
+++ b/todo.md
@@ -1,19 +1,16 @@
# HIGH PRIORITY
+- trial system (IN HOUSE)
+
- BUGS:
- FEATURES:
-- make login/register into a modal that appears atop the landing page
-- user preferences
- - make pixel the default icon scheme
# LOW PRIORITY
-- organisation
- - see members' time tracking numbers
- - export times to csv, json, etc.
- issues
- assignee "note" for extra context on their role in the task
- deadline
+ - attachments (description or comment)
- user preferences
- colour scheme
- "assign to me by default" option for new issues
@@ -25,7 +22,19 @@
- pull request (github/gitlab/bitbucket)
- view:
- open git diff in a new tab
-- figure out if it's possible to remove the "lib/server/..." helpers altogether, and have some sort of dynamic route maker in the shared package
-- request logging
-- explore payment providers (stripe is the only one i know)
-- trial system
\ No newline at end of file
+- trial system
+
+# PRO BENEFITS COMING SOON:
+
+- git integration
+
+## still need to lock these behid paywall:
+
+- Advanced time tracking & reports
+- Custom issue types
+
+# EMAILS:
+
+- welcome to sprint
+- thank you for subscribing to Sprint Pro. here is what you get.
+- your trial is coming to an end. manage your subscription to renew.
From 2aa13e34bfa7af2f306a1a12ebabcee8dec67b1b Mon Sep 17 00:00:00 2001
From: Oliver Bryan
Date: Thu, 29 Jan 2026 15:20:42 +0000
Subject: [PATCH 4/4] removed all free tier restrictions
---
packages/backend/.env.example | 4 +-
packages/backend/src/routes/issue/create.ts | 28 +-
.../src/routes/organisation/add-member.ts | 26 +-
.../backend/src/routes/organisation/create.ts | 28 +-
.../organisation/member-time-tracking.ts | 22 +-
packages/backend/src/routes/project/create.ts | 27 +-
packages/backend/src/routes/sprint/create.ts | 30 +-
packages/backend/src/routes/user/update.ts | 24 +-
.../backend/src/routes/user/upload-avatar.ts | 28 +-
packages/frontend/src/components/account.tsx | 41 +-
.../frontend/src/components/issue-form.tsx | 34 +-
.../src/components/organisation-select.tsx | 31 +-
.../frontend/src/components/organisations.tsx | 125 +++---
.../src/components/project-select.tsx | 29 +-
.../frontend/src/components/sprint-form.tsx | 16 +-
packages/frontend/src/components/top-bar.tsx | 8 +-
.../frontend/src/components/upload-avatar.tsx | 114 +++---
packages/frontend/src/main.tsx | 6 +-
packages/frontend/src/pages/BoringStuff.tsx | 4 +-
packages/frontend/src/pages/Landing.tsx | 62 +--
packages/frontend/src/pages/Plans.tsx | 365 ++++++------------
21 files changed, 413 insertions(+), 639 deletions(-)
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 }) {
-
- Icon Style
-
- setIconPreference(v as IconStyle)}
- disabled={currentUser.plan !== "pro"}
- >
-
+ Icon Style
+ 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 !== "" &&
{error} }
- {/* Show subscription management link */}
-
+ {/* subscription management link commented out for beta */}
+ {/*
{currentUser.plan === "pro" ? (
Manage subscription
@@ -204,7 +185,7 @@ function Account({ trigger }: { trigger?: ReactNode }) {
Upgrade to Pro
)}
-
+
*/}
diff --git a/packages/frontend/src/components/issue-form.tsx b/packages/frontend/src/components/issue-form.tsx
index 337bcb1..8c09e4d 100644
--- a/packages/frontend/src/components/issue-form.tsx
+++ b/packages/frontend/src/components/issue-form.tsx
@@ -2,7 +2,7 @@ import { ISSUE_DESCRIPTION_MAX_LENGTH, ISSUE_TITLE_MAX_LENGTH } from "@sprint/sh
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 { MultiAssigneeSelect } from "@/components/multi-assignee-select";
import { useAuthenticatedSession } from "@/components/session-provider";
import { SprintSelect } from "@/components/sprint-select";
@@ -24,7 +24,7 @@ import { Label } from "@/components/ui/label";
import { SelectTrigger } from "@/components/ui/select";
import {
useCreateIssue,
- useIssues,
+ // useIssues,
useOrganisationMembers,
useSelectedOrganisation,
useSelectedProject,
@@ -33,7 +33,7 @@ import {
import { parseError } from "@/lib/server";
import { cn, issueID } from "@/lib/utils";
-const FREE_TIER_ISSUE_LIMIT = 100;
+// const free_tier_issue_limit = 100;
export function IssueForm({ trigger }: { trigger?: React.ReactNode }) {
const { user } = useAuthenticatedSession();
@@ -41,12 +41,12 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) {
const selectedProject = useSelectedProject();
const { data: sprints = [] } = useSprints(selectedProject?.Project.id);
const { data: membersData = [] } = useOrganisationMembers(selectedOrganisation?.Organisation.id);
- const { data: issues = [] } = useIssues(selectedProject?.Project.id);
+ // const { data: issues = [] } = useIssues(selectedProject?.Project.id);
const createIssue = useCreateIssue();
- const isPro = user.plan === "pro";
- const issueCount = issues.length;
- const isAtIssueLimit = !isPro && issueCount >= FREE_TIER_ISSUE_LIMIT;
+ // const isPro = user.plan === "pro";
+ // const issueCount = issues.length;
+ // const isAtIssueLimit = !isPro && issueCount >= FREE_TIER_ISSUE_LIMIT;
const members = useMemo(() => membersData.map((member) => member.User), [membersData]);
const statuses = selectedOrganisation?.Organisation.statuses ?? {};
@@ -149,14 +149,8 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) {
{trigger || (
Create Issue
@@ -168,7 +162,7 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) {
Create Issue
- {!isPro && selectedProject && (
+ {/* {!isPro && selectedProject && (
- )}
+ )} */}
- {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={
- = FREE_TIER_LIMITS.membersPerOrganisation}
- title={
- !isPro && memberCount >= FREE_TIER_LIMITS.membersPerOrganisation
- ? "Free tier limited to 5 members per organisation. Upgrade to Pro for unlimited."
- : undefined
- }
- >
+
Add user
}
@@ -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" && (
Upgrade
- )}
+ )} */}
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(
- ,
- {
- 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
-
+ {/*
See pricing
-
+ */}
>
)}
- 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("monthly")}
- className={cn(
- "text-lg transition-colors",
- billingPeriod === "monthly" ? "text-foreground font-700" : "text-muted-foreground",
- )}
- >
- monthly
-
- 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"
- />
- setBillingPeriod("annual")}
- className={cn(
- "text-lg transition-colors",
- billingPeriod === "annual" ? "text-foreground font-700" : "text-muted-foreground",
- )}
- >
- annual
-
-
- 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);
- }}
- >
-
-
- {isCancellationScheduled || isCanceled
- ? "Cancellation scheduled"
- : "Cancel subscription"}
-
-
-
-
- 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 */}