Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: small stuff after new pricing (#562)

* wip:

* refactor: plans package

* fix: type error

authored by

Maximilian Kaske and committed by
GitHub
35154613 bceb29a5

+93 -91
+9 -5
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/billing/_components/plan.tsx
··· 14 14 export const SettingsPlan = ({ workspace }: { workspace: Workspace }) => { 15 15 const router = useRouter(); 16 16 const [isPending, startTransition] = useTransition(); 17 - const [isPortalPending, startPortalTransition] = useTransition(); 18 17 19 18 const getCheckoutSession = (plan: WorkspacePlan) => { 20 19 startTransition(async () => { ··· 32 31 }; 33 32 34 33 const getUserCustomerPortal = () => { 35 - startPortalTransition(async () => { 34 + startTransition(async () => { 36 35 const url = await api.stripeRouter.getUserCustomerPortal.mutate({ 37 36 workspaceSlug: workspace.slug, 38 37 }); ··· 46 45 <div className="grid gap-4"> 47 46 <div className="grid gap-6"> 48 47 <div> 49 - <Button onClick={getUserCustomerPortal} variant="outline"> 50 - {isPortalPending ? ( 48 + <Button 49 + onClick={getUserCustomerPortal} 50 + variant="outline" 51 + disabled={isPending} 52 + > 53 + {isPending ? ( 51 54 <LoadingAnimation variant="inverse" /> 52 55 ) : ( 53 56 "Customer Portal" ··· 58 61 currentPlan={workspace.plan} 59 62 isLoading={isPending} 60 63 events={{ 61 - free: () => getCheckoutSession("free"), 64 + // REMINDER: redirecting to customer portal as a fallback because the free plan has no price 65 + free: getUserCustomerPortal, 62 66 starter: () => getCheckoutSession("starter"), 63 67 pro: () => getCheckoutSession("pro"), 64 68 team: () => getCheckoutSession("team"),
+2 -13
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/team/page.tsx
··· 1 - import { AlertTriangle } from "lucide-react"; 2 - 3 - import { Alert, AlertDescription, AlertTitle } from "@openstatus/ui"; 4 - 1 + import { ProFeatureAlert } from "@/components/billing/pro-feature-alert"; 5 2 import { columns as invitationColumns } from "@/components/data-table/invitation/columns"; 6 3 import { DataTable as InvitationDataTable } from "@/components/data-table/invitation/data-table"; 7 4 import { columns as userColumns } from "@/components/data-table/user/columns"; ··· 18 15 19 16 return ( 20 17 <div className="flex flex-col gap-4"> 21 - {isFreePlan ? ( 22 - <Alert> 23 - <AlertTriangle className="h-4 w-4" /> 24 - <AlertTitle>Team</AlertTitle> 25 - <AlertDescription> 26 - Please upgrade to invite more team members. 27 - </AlertDescription> 28 - </Alert> 29 - ) : null} 18 + {isFreePlan ? <ProFeatureAlert feature="Team members" /> : null} 30 19 {/* TODO: only show if isAdmin */} 31 20 <div className="flex justify-end"> 32 21 <InviteButton disabled={isFreePlan} />
+18 -17
apps/web/src/app/status-page/[domain]/layout.tsx
··· 25 25 const page = await api.page.getPageBySlug.query({ slug: params.domain }); 26 26 if (!page) return notFound(); 27 27 28 + const plan = page.workspacePlan; 29 + const isSubscribers = allPlans[plan].limits["status-subscribers"]; 30 + const isWhiteLabel = allPlans[plan].limits["white-label"]; 31 + 28 32 const navigation = [ 29 33 { 30 34 label: "Status", ··· 37 41 href: setPrefixUrl("/incidents", params), 38 42 }, 39 43 ]; 40 - 41 - const isSubscribersValid = 42 - allPlans[page.workspacePlan].limits["status-subscribers"]; 43 44 44 45 return ( 45 46 <div className="flex min-h-screen w-full flex-col space-y-6 p-4 md:p-8"> ··· 60 61 </div> 61 62 <Navbar navigation={navigation} /> 62 63 <div className="text-end sm:w-[100px]"> 63 - {isSubscribersValid ? ( 64 - <SubscribeButton slug={params.domain} /> 65 - ) : null} 64 + {isSubscribers ? <SubscribeButton slug={params.domain} /> : null} 66 65 </div> 67 66 </Shell> 68 67 </header> ··· 73 72 </main> 74 73 <footer className="z-10 mx-auto flex w-full max-w-xl items-center justify-between"> 75 74 <div /> 76 - <p className="text-muted-foreground text-center text-sm"> 77 - powered by{" "} 78 - <a 79 - href="https://www.openstatus.dev" 80 - target="_blank" 81 - rel="noreferrer" 82 - className="text-foreground underline underline-offset-4 hover:no-underline" 83 - > 84 - openstatus.dev 85 - </a> 86 - </p> 75 + {!isWhiteLabel ? ( 76 + <p className="text-muted-foreground text-center text-sm"> 77 + powered by{" "} 78 + <a 79 + href="https://www.openstatus.dev" 80 + target="_blank" 81 + rel="noreferrer" 82 + className="text-foreground underline underline-offset-4 hover:no-underline" 83 + > 84 + openstatus.dev 85 + </a> 86 + </p> 87 + ) : null} 87 88 <ThemeToggle /> 88 89 </footer> 89 90 </div>
+2 -2
apps/web/src/components/billing/pro-feature-alert.tsx
··· 15 15 return ( 16 16 <Alert> 17 17 <AlertTriangle className="h-4 w-4" /> 18 - <AlertTitle>{feature} are a Pro feature.</AlertTitle> 18 + <AlertTitle>{feature} is a Pro feature.</AlertTitle> 19 19 <AlertDescription> 20 20 If you want to use{" "} 21 21 <span className="underline decoration-dotted">{feature}</span>, please 22 - upgrade to the Pro plan. Go to{" "} 22 + upgrade your plan. Go to{" "} 23 23 <Link 24 24 href={`/app/${params.workspaceSlug}/settings/billing`} 25 25 className="text-foreground inline-flex items-center font-medium underline underline-offset-4 hover:no-underline"
+1 -1
apps/web/src/components/marketing/faqs.tsx
··· 11 11 const faqsConfig: Record<"q" | "a", string>[] = [ 12 12 { 13 13 q: "What are the limits?", 14 - a: "You will start with a free plan by default which includes a total of <strong>5 monitors</strong> and <strong>1 status</strong> page as well as cron jobs of either <code>10m</code>, <code>30m</code> or <code>1h</code>.", 14 + a: "You will start with a free plan by default which includes a total of <strong>3 monitors</strong> and <strong>1 status</strong> page as well as cron jobs of either <code>10m</code>, <code>30m</code> or <code>1h</code>.<br />Learn more about our <a href='/pricing'>pricing</a>.", 15 15 }, 16 16 { 17 17 q: "Who are we?",
+4 -4
apps/web/src/components/marketing/pricing/pricing-table.tsx
··· 4 4 import { useRouter } from "next/navigation"; 5 5 import { Check } from "lucide-react"; 6 6 7 - import type { PlanName } from "@openstatus/plans"; 7 + import type { WorkspacePlan } from "@openstatus/plans"; 8 8 import { 9 9 allPlans, 10 10 plans as defaultPlans, ··· 31 31 events, 32 32 isLoading, 33 33 }: { 34 - plans?: readonly PlanName[]; 35 - currentPlan?: PlanName; 36 - events?: Partial<Record<PlanName, () => void>>; 34 + plans?: readonly WorkspacePlan[]; 35 + currentPlan?: WorkspacePlan; 36 + events?: Partial<Record<WorkspacePlan, () => void>>; 37 37 isLoading?: boolean; 38 38 }) { 39 39 const router = useRouter();
+2 -2
apps/web/src/components/marketing/pricing/pricing-wrapper.tsx
··· 2 2 3 3 import { useSearchParams } from "next/navigation"; 4 4 5 - import type { PlanName } from "@openstatus/plans"; 5 + import type { WorkspacePlan } from "@openstatus/plans"; 6 6 7 7 import { PricingPlanRadio } from "./pricing-plan-radio"; 8 8 import { PricingTable } from "./pricing-table"; ··· 14 14 <div className="flex flex-col gap-4 sm:hidden"> 15 15 <PricingPlanRadio /> 16 16 <PricingTable 17 - plans={[(searchParams.get("plan") as PlanName) || "team"]} 17 + plans={[(searchParams.get("plan") as WorkspacePlan) || "team"]} 18 18 /> 19 19 </div> 20 20 <div className="hidden sm:block">
+2 -4
packages/api/src/router/invitation.ts
··· 19 19 .mutation(async (opts) => { 20 20 const { email } = opts.input; 21 21 22 - const membersLimit = 23 - allPlans[opts.ctx.workspace.plan].limits.members === "unlimited" 24 - ? 420 25 - : 1; 22 + const _members = allPlans[opts.ctx.workspace.plan].limits.members; 23 + const membersLimit = _members === "Unlimited" ? 420 : _members; 26 24 27 25 const usersToWorkspacesNumbers = ( 28 26 await opts.ctx.db.query.usersToWorkspaces.findMany({
+12 -36
packages/plans/index.ts packages/plans/src/config.ts
··· 1 - import type { MonitorPeriodicity } from "@openstatus/db/src/schema"; 2 - 3 - export const plans = ["free", "starter", "team", "pro"] as const; 4 - export type PlanName = (typeof plans)[number]; 5 - 6 - export type Limits = { 7 - // monitors 8 - monitors: number; 9 - periodicity: Partial<MonitorPeriodicity>[]; 10 - "multi-region": boolean; 11 - "data-retention": string; 12 - // status pages 13 - "status-pages": number; 14 - "status-subscribers": boolean; 15 - "custom-domain": boolean; 16 - "white-label": boolean; 17 - // alerts 18 - notifications: boolean; 19 - sms: boolean; 20 - "notification-channels": number; 21 - // collaboration 22 - members: string; 23 - "audit-log": boolean; 24 - }; 25 - 26 - export type FeatureKey = keyof Limits; 1 + import type { WorkspacePlan } from "@openstatus/db/src/schema"; 27 2 28 - export type Plan = { 29 - title: string; 30 - description: string; 31 - price: number; 32 - limits: Limits; 33 - }; 3 + import type { Limits } from "./types"; 34 4 35 5 // TODO: rename to `planConfig` 36 - export const allPlans: Record<PlanName, Plan> = { 6 + export const allPlans: Record< 7 + WorkspacePlan, 8 + { 9 + title: string; 10 + description: string; 11 + price: number; 12 + limits: Limits; 13 + } 14 + > = { 37 15 free: { 38 16 title: "Hobby", 39 17 description: "For personal projects", ··· 50 28 notifications: true, 51 29 sms: false, 52 30 "notification-channels": 1, 53 - members: "1", 31 + members: 1, 54 32 "audit-log": false, 55 33 }, 56 34 }, ··· 115 93 }, 116 94 }, 117 95 }; 118 - 119 - export { pricingTableConfig } from "./pricing-table";
+1 -1
packages/plans/package.json
··· 2 2 "name": "@openstatus/plans", 3 3 "version": "1.0.0", 4 4 "description": "", 5 - "main": "index.ts", 5 + "main": "src/index.ts", 6 6 "scripts": {}, 7 7 "dependencies": { 8 8 "@openstatus/db": "workspace:*",
+4 -6
packages/plans/pricing-table.ts packages/plans/src/pricing-table.ts
··· 1 - import type { FeatureKey } from "./index"; 1 + import type { Limits } from "./types"; 2 2 3 - type TableConfig = Record< 3 + export const pricingTableConfig: Record< 4 4 string, 5 5 { 6 6 label: string; 7 - features: { value: FeatureKey; label: string; badge?: string }[]; 7 + features: { value: keyof Limits; label: string; badge?: string }[]; 8 8 } 9 - >; 10 - 11 - export const pricingTableConfig: TableConfig = { 9 + > = { 12 10 monitors: { 13 11 label: "Monitors", 14 12 features: [
+6
packages/plans/src/index.ts
··· 1 + export { allPlans } from "./config"; 2 + export { getLimit } from "./utils"; 3 + export { pricingTableConfig } from "./pricing-table"; 4 + 5 + export { workspacePlans as plans } from "@openstatus/db/src/schema"; 6 + export type { WorkspacePlan } from "@openstatus/db/src/schema";
+21
packages/plans/src/types.ts
··· 1 + import type { MonitorPeriodicity } from "@openstatus/db/src/schema"; 2 + 3 + export type Limits = { 4 + // monitors 5 + monitors: number; 6 + periodicity: Partial<MonitorPeriodicity>[]; 7 + "multi-region": boolean; 8 + "data-retention": string; 9 + // status pages 10 + "status-pages": number; 11 + "status-subscribers": boolean; 12 + "custom-domain": boolean; 13 + "white-label": boolean; 14 + // alerts 15 + notifications: boolean; 16 + sms: boolean; 17 + "notification-channels": number; 18 + // collaboration 19 + members: "Unlimited" | number; 20 + "audit-log": boolean; 21 + };
+9
packages/plans/src/utils.ts
··· 1 + import type { WorkspacePlan } from "@openstatus/db/src/schema"; 2 + 3 + import { allPlans } from "./index"; 4 + import type { Limits } from "./types"; 5 + 6 + // TODO: use getLimit utils function 7 + export function getLimit(plan: WorkspacePlan, limit: keyof Limits) { 8 + return allPlans[plan].limits[limit]; 9 + }