Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/portfolio/app/(workspace)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { PortfolioWorkspaceBootstrap } from "@/store/portfolio-store";

import { fetchServerApiData } from "@/lib/server-api";

import { RestrictedAccess } from "@/components/RestrictedAccess";
import { WorkspaceProvider } from "@/components/WorkspaceProvider";

export default async function WorkspaceLayout({ children }: { children: React.ReactNode }) {
Expand All @@ -9,6 +12,12 @@ export default async function WorkspaceLayout({ children }: { children: React.Re
fetchServerApiData<PortfolioWorkspaceBootstrap["analytics"]>("/portfolios/analytics"),
]);

const isProd = process.env.NODE_ENV === "production";
const adminEmail = (process.env.ADMIN_EMAIL || "ashragautam25@gmail.com").toLowerCase();
const isUserAdmin = user && user.email && user.email.toLowerCase() === adminEmail;

if (isProd && user && !isUserAdmin) return <RestrictedAccess />;

return (
<WorkspaceProvider initialData={{ user, workspace, analytics }}>{children}</WorkspaceProvider>
);
Expand Down
14 changes: 13 additions & 1 deletion apps/portfolio/app/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";

import { siteConfig } from "@/config/site";
import { fetchServerApiData } from "@/lib/server-api";

import { pricingFaqs } from "@/features/faq/constants";

Expand Down Expand Up @@ -48,7 +49,13 @@ export const metadata: Metadata = {
},
};

export default function PricingPage() {
export default async function PricingPage() {
const user = await fetchServerApiData<{ email: string | null }>("/users/me");
const isProd = process.env.NODE_ENV === "production";
const adminEmail = (process.env.ADMIN_EMAIL || "ashragautam25@gmail.com").toLowerCase();
const isAdmin = user && user.email && user.email.toLowerCase() === adminEmail;
const paymentsBlocked = isProd && !isAdmin;

const pricingSchema = {
"@context": "https://schema.org",
"@type": "Product",
Expand Down Expand Up @@ -159,6 +166,11 @@ export default function PricingPage() {
<Navigation />

<main className="relative z-10 w-full max-w-full overflow-x-clip pt-28">
{paymentsBlocked && (
<div className="mx-auto w-[min(1160px,calc(100%-32px))] mt-8 rounded-2xl border border-warning bg-warning-soft/30 p-4 text-sm font-semibold text-warning">
Payments are disabled in production during this phase. Only system administrators can perform checkouts.
</div>
)}
<BundlePricingSection />
<CustomPlansSection />
<ComparisonTable />
Expand Down
64 changes: 64 additions & 0 deletions apps/portfolio/components/RestrictedAccess.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import Link from "next/link";
import { Lock, ArrowLeft, ArrowRight, AppWindow } from "lucide-react";

import { siteConfig, veriworklyProductLinks } from "@/config/site";

export function RestrictedAccess() {
return (
<main className="text-ink-2 selection:bg-accent selection:text-accent-ink bg-paper relative flex min-h-dvh flex-col items-center justify-center overflow-x-clip px-6 py-12">
<div className="bg-accent/10 pointer-events-none absolute top-1/2 left-1/2 size-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full blur-[120px]" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(var(--color-ink)_0.8px,transparent_0.8px)] bg-size-[24px_24px] opacity-[0.06]" />

<div className="border-line bg-panel relative z-10 w-full max-w-lg rounded-3xl border p-8 text-center shadow-lg md:p-12">
<div className="border-line-strong bg-paper mx-auto flex h-16 w-16 items-center justify-center rounded-2xl border shadow-sm">
<Lock className="text-accent h-7 w-7" />
</div>

<h1 className="text-ink mt-8 text-3xl font-bold tracking-tight">Private Preview Mode</h1>

<p className="text-muted mt-4 text-sm leading-relaxed">
The VeriWorkly Portfolio Builder is currently in active development. To guarantee platform
stability and design refinement, production access is restricted to administrators.
</p>

<div className="border-line bg-paper/50 mt-8 rounded-2xl border p-5 text-left">
<h2 className="text-ink flex items-center gap-2 text-sm font-semibold">
<AppWindow className="text-accent h-4 w-4" /> What can I do?
</h2>
<ul className="text-muted mt-3 space-y-2.5 text-xs font-medium">
<li className="flex items-start gap-2">
<span className="text-accent mt-0.5">•</span>
<span>Visit published portfolios (e.g. template examples)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-0.5">•</span>
<span>Access public pages like templates, FAQ, and pricing</span>
</li>
<li className="flex items-start gap-2">
<span className="text-accent mt-0.5">•</span>
<span>Manage your resumes and document tools in VeriWorkly Studio</span>
</li>
</ul>
</div>

<div className="mt-8 flex flex-col gap-3">
<Link
href={veriworklyProductLinks.studio}
className="bg-accent hover:bg-accent-strong flex items-center justify-center gap-2 rounded-xl px-5 py-3.5 text-sm font-bold text-white shadow-md transition-colors duration-200"
>
Go to VeriWorkly Studio <ArrowRight className="h-4 w-4" />
</Link>

<Link
href={siteConfig.links.main}
className="border-line text-ink-soft hover:bg-paper-2 flex items-center justify-center gap-2 rounded-xl border px-5 py-3.5 text-sm font-bold transition-colors duration-200"
>
<ArrowLeft className="h-4 w-4" /> Back to Homepage
</Link>
</div>
</div>
</main>
);
}
42 changes: 35 additions & 7 deletions apps/server/src/controllers/billingController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { NextFunction, Request, Response } from "express";
import { z } from "zod";

import { requireAuthUser } from "#middleware/auth";
import { config } from "#config";

import { BillingService } from "#services/billingService";
import { CreditService } from "#services/creditService";
Expand Down Expand Up @@ -52,10 +53,20 @@ export class BillingController {
try {
const input = checkoutSchema.parse(req.body);

const isProd = config.nodeEnv === "production";
const user = requireAuthUser(req);
const isAdmin = config.admin.email && user.email?.toLowerCase() === config.admin.email;

if (isProd && !isAdmin)
throw new ApiError(
403,
"Payments are disabled in production during this phase. Only administrators can perform payments.",
);

res.json(
createSuccessResponse(
await BillingService.createCheckout(
requireAuthUser(req).id,
user.id,
input.productKey,
input.interval,
input.redirectUrl,
Expand All @@ -77,7 +88,17 @@ export class BillingController {

static async portal(req: Request, res: Response, next: NextFunction) {
try {
res.json(createSuccessResponse(await BillingService.createPortal(requireAuthUser(req).id)));
const isProd = config.nodeEnv === "production";
const user = requireAuthUser(req);
const isAdmin = config.admin.email && user.email?.toLowerCase() === config.admin.email;

if (isProd && !isAdmin)
throw new ApiError(
403,
"Payments are disabled in production during this phase. Only administrators can perform payments.",
);

res.json(createSuccessResponse(await BillingService.createPortal(user.id)));
} catch (error) {
next(error);
}
Expand All @@ -86,13 +107,20 @@ export class BillingController {
static async creditPackCheckout(req: Request, res: Response, next: NextFunction) {
try {
const input = creditPackCheckoutSchema.parse(req.body);

const isProd = config.nodeEnv === "production";
const user = requireAuthUser(req);
const isAdmin = config.admin.email && user.email?.toLowerCase() === config.admin.email;

if (isProd && !isAdmin)
throw new ApiError(
403,
"Payments are disabled in production during this phase. Only administrators can perform payments.",
);

res.json(
createSuccessResponse(
await BillingService.createCreditPackCheckout(
requireAuthUser(req).id,
input.packKey,
input.redirectUrl,
),
await BillingService.createCreditPackCheckout(user.id, input.packKey, input.redirectUrl),
),
);
} catch (error) {
Expand Down
47 changes: 41 additions & 6 deletions apps/site/app/pricing/pricing-experience.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useState, useEffect } from "react";
import {
ArrowRight,
Check,
Expand Down Expand Up @@ -67,6 +67,25 @@
const [customPlan, setCustomPlan] = useState<"portfolio_pro" | "ai_credits">("portfolio_pro");
const [loading, setLoading] = useState("");
const [error, setError] = useState("");
const [user, setUser] = useState<{ email?: string | null } | null>(null);
const [checkingUser, setCheckingUser] = useState(true);

Check warning on line 71 in apps/site/app/pricing/pricing-experience.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format Check

'checkingUser' is assigned a value but never used

useEffect(() => {
fetchApiData<{ email: string | null; name: string | null }>("/users/me")
.then((data) => {
setUser(data);
setCheckingUser(false);
})
.catch(() => {
setUser(null);
setCheckingUser(false);
});
}, []);

const isProd = process.env.NODE_ENV === "production";
const adminEmail = (process.env.NEXT_PUBLIC_ADMIN_EMAIL || "ashragautam25@gmail.com").toLowerCase();
const isAdmin = user && user.email && user.email.toLowerCase() === adminEmail;
const paymentsBlocked = isProd && !isAdmin;

const checkout = async (productKey: ProductKey, interval: BillingInterval) => {
const checkoutKey = `${productKey}:${interval}`;
Expand Down Expand Up @@ -131,6 +150,12 @@
</div>
</div>

{paymentsBlocked ? (
<div className="mt-8 rounded-2xl border border-amber-500/25 bg-amber-500/10 p-4 text-sm font-bold text-amber-800 dark:text-amber-300">
Payments are disabled in production during this phase. Only system administrators can perform checkouts.
</div>
) : null}

{error ? (
<p className="mt-8 rounded-2xl border border-red-500/25 bg-red-500/10 p-4 text-sm font-bold text-red-700 dark:text-red-300">
{error}
Expand All @@ -146,6 +171,7 @@
description="A tiny commitment for one focused application, portfolio update, or deadline."
features={primaryFeatures}
loading={loading === "bundle:one_day"}
disabled={paymentsBlocked}
onCheckout={() => void checkout("bundle", "one_day")}
/>
<PriceCard
Expand All @@ -156,6 +182,7 @@
description="Enough runway to refine the full story, apply broadly, and publish with confidence."
features={primaryFeatures}
loading={loading === "bundle:seven_day"}
disabled={paymentsBlocked}
onCheckout={() => void checkout("bundle", "seven_day")}
/>
<PriceCard
Expand All @@ -169,6 +196,7 @@
note={bundlePrice.note}
badge={bundlePrice.savings}
loading={loading === `bundle:${bundleInterval}`}
disabled={paymentsBlocked}
onCheckout={() => void checkout("bundle", bundleInterval)}
toggle={<IntervalToggle value={bundleInterval} onChange={setBundleInterval} />}
/>
Expand Down Expand Up @@ -239,9 +267,10 @@
<CheckoutButton
className="mt-5 w-full bg-[#171713] text-white dark:bg-[#f7f4ec] dark:text-[#171713]"
loading={loading === `${customPlan}:monthly`}
disabled={paymentsBlocked}
onClick={() => void checkout(customPlan, "monthly")}
>
Choose {selectedCustom.title}
{paymentsBlocked ? "Payments disabled" : `Choose ${selectedCustom.title}`}
</CheckoutButton>
</div>
</div>
Expand Down Expand Up @@ -305,9 +334,10 @@
<CheckoutButton
className="bg-[#171713] text-white"
loading={loading === "bundle:annual"}
disabled={paymentsBlocked}
onClick={() => void checkout("bundle", "annual")}
>
Get the yearly bundle
{paymentsBlocked ? "Payments disabled" : "Get the yearly bundle"}
</CheckoutButton>
</div>
</div>
Expand Down Expand Up @@ -354,6 +384,7 @@
loading,
toggle,
onCheckout,
disabled,
}: {
marker: string;
title: string;
Expand All @@ -367,6 +398,7 @@
loading: boolean;
toggle?: React.ReactNode;
onCheckout: () => void;
disabled?: boolean;
}) {
return (
<article
Expand Down Expand Up @@ -410,9 +442,10 @@
: "bg-[#171713] text-white dark:bg-[#f7f4ec] dark:text-[#171713]"
}`}
loading={loading}
disabled={disabled}
onClick={onCheckout}
>
Choose {title}
{disabled ? "Payments disabled" : `Choose ${title}`}
</CheckoutButton>
</article>
);
Expand All @@ -423,16 +456,18 @@
className,
loading,
onClick,
disabled,
}: {
children: React.ReactNode;
className: string;
loading: boolean;
onClick: () => void;
disabled?: boolean;
}) {
return (
<button
className={`inline-flex min-h-12 items-center justify-center gap-2 rounded-full px-5 text-sm font-black transition hover:-translate-y-0.5 disabled:cursor-wait disabled:opacity-65 ${className}`}
disabled={loading}
className={`inline-flex min-h-12 items-center justify-center gap-2 rounded-full px-5 text-sm font-black transition hover:-translate-y-0.5 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
disabled={loading || disabled}
onClick={onClick}
type="button"
>
Expand Down
Loading
Loading