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
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
- do never commit unless you're asked to.
- only commit what you're asked to commit, no adding extra changes unless asked
- when person asks the question, but not asking you to do the thing, you only answer the question, you don't do the thing you're not asked to do!
- STOP before any file edit. Ask yourself: "Did they explicitly say to make changes?" If no → DO NOT EDIT. Present findings and wait.
- "find", "check", "investigate", "research", "explain", "look into" = RESEARCH ONLY. Never edit code.
- The ONLY trigger for code changes is explicit words like: "do it", "fix it", "implement", "make the change", "go ahead"
1 change: 1 addition & 0 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"@base-ui/react": "^1.2.0",
"@paykitjs/dash": "workspace:^",
"@paykitjs/polar": "workspace:*",
"@paykitjs/stripe": "workspace:*",
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.69.0",
Expand Down
14 changes: 1 addition & 13 deletions apps/demo/src/app/_components/auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export function AuthForm({ redirectTo }: { redirectTo: string }) {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [isSignUp, setIsSignUp] = useState(false);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
Expand All @@ -28,7 +27,7 @@ export function AuthForm({ redirectTo }: { redirectTo: string }) {
const result = await authClient.signUp.email({
email,
password,
name,
name: "Demo User",
});
if (result.error) {
setError(result.error.message ?? "Sign up failed");
Expand Down Expand Up @@ -60,17 +59,6 @@ export function AuthForm({ redirectTo }: { redirectTo: string }) {
</CardHeader>
<CardContent>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
{isSignUp ? (
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
onChange={(event) => setName(event.target.value)}
placeholder="Your name"
value={name}
/>
</div>
) : null}
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
Expand Down
6 changes: 3 additions & 3 deletions apps/demo/src/app/_components/subscribe-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import type { PayKit } from "@/lib/paykit";
import { paykitClient } from "@/lib/paykit-client";
import type { PayKit } from "@/server/paykit";
import { api, type RouterOutputs } from "@/trpc/react";

type SubscribePlanId = PayKit["planId"];
Expand Down Expand Up @@ -170,7 +170,7 @@ function TestClockPanel() {
{testClock.data ? <Badge variant="outline">{testClock.data.status}</Badge> : null}
</CardTitle>
<CardDescription>
Advance the logged-in customer through Stripe billing cycles without leaving the demo.
Advance the logged-in customer through billing cycles without leaving the demo.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-3">
Expand All @@ -191,7 +191,7 @@ function TestClockPanel() {
<span className="text-lg font-semibold">
{formatDateTime(testClock.data.frozenTime)}
</span>
<span className="text-muted-foreground text-sm">Stripe time</span>
<span className="text-muted-foreground text-sm">Test clock</span>
</div>
<div className="flex flex-wrap gap-2">
{actions.map((action) => (
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/app/paykit/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { paykitHandler } from "paykitjs/handlers/next";

import { paykit } from "@/server/paykit";
import { paykit } from "@/lib/paykit";

export const { GET, POST } = paykitHandler(paykit);
10 changes: 7 additions & 3 deletions apps/demo/src/env.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import * as z from "zod";

export const env = createEnv({
/**
Expand All @@ -10,8 +10,10 @@ export const env = createEnv({
APP_URL: z.string().url(),
DATABASE_URL: z.string().min(1),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
STRIPE_SECRET_KEY: z.string().min(1),
STRIPE_WEBHOOK_SECRET: z.string().min(1),
STRIPE_SECRET_KEY: z.string().min(1).optional(),
STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(),
POLAR_ACCESS_TOKEN: z.string().min(1),
POLAR_WEBHOOK_SECRET: z.string().min(1),
BETTER_AUTH_SECRET: z.string().min(1),
AUTUMN_SECRET_KEY: z.string().min(1).optional(),
},
Expand All @@ -35,6 +37,8 @@ export const env = createEnv({
NODE_ENV: process.env.NODE_ENV,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
POLAR_ACCESS_TOKEN: process.env.POLAR_ACCESS_TOKEN,
POLAR_WEBHOOK_SECRET: process.env.POLAR_WEBHOOK_SECRET,
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
AUTUMN_SECRET_KEY: process.env.AUTUMN_SECRET_KEY,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/lib/paykit-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createPayKitClient } from "paykitjs/client";

import type { paykit } from "@/server/paykit";
import type { paykit } from "@/lib/paykit";

export const paykitClient = createPayKitClient<typeof paykit>();
File renamed without changes.
16 changes: 11 additions & 5 deletions apps/demo/src/server/paykit.ts → apps/demo/src/lib/paykit.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { dash } from "@paykitjs/dash";
import { stripe } from "@paykitjs/stripe";
import { polar } from "@paykitjs/polar";
// import { stripe } from "@paykitjs/stripe";
import { createPayKit } from "paykitjs";

import { env } from "@/env";
import { auth } from "@/server/auth";
import { pool } from "@/server/db";

import { free, pro, ultra } from "./plans";
import { free, pro, ultra } from "./paykit-products";

export const paykit = createPayKit({
testing: { enabled: true },
database: pool,
provider: stripe({
secretKey: env.STRIPE_SECRET_KEY,
webhookSecret: env.STRIPE_WEBHOOK_SECRET,
// provider: stripe({
// secretKey: env.STRIPE_SECRET_KEY,
// webhookSecret: env.STRIPE_WEBHOOK_SECRET,
// }),
provider: polar({
accessToken: env.POLAR_ACCESS_TOKEN,
webhookSecret: env.POLAR_WEBHOOK_SECRET,
server: "sandbox",
}),
plans: [pro, ultra, free],
plugins: [
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { autumnRouter } from "@/server/api/routers/autumn";
import { paykitRouter } from "@/server/api/routers/paykit";
import { paykitRouter } from "@/server/api/routers/paykit-route";
import { postRouter } from "@/server/api/routers/post";
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";

import { paykit } from "@/lib/paykit";
import type { PayKit } from "@/lib/paykit";
import { createTRPCRouter, publicProcedure } from "@/server/api/trpc";
import { auth } from "@/server/auth";
import { paykit, type PayKit } from "@/server/paykit";

export const paykitRouter = createTRPCRouter({
createCustomer: publicProcedure.mutation(async ({ ctx }) => {
Expand Down
15 changes: 6 additions & 9 deletions e2e/cli/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";

import { config } from "dotenv";
import { Pool } from "pg";
import { default as Stripe } from "stripe";

process.env.PAYKIT_CLI = "1";
import { env } from "../env";

// Load env from repo root
config({ path: path.resolve(import.meta.dirname, "../../.env") });
config({ path: path.resolve(import.meta.dirname, "../../.env.local"), override: true });
process.env.PAYKIT_CLI = "1";

const packageRoot = path.resolve(import.meta.dirname, "../../packages/paykit");
const createPayKitPath = path.resolve(packageRoot, "src/index.ts");
Expand All @@ -29,17 +26,17 @@ export interface CliTestFixture {
* Postgres DB and real Stripe. Returns everything needed for cleanup.
*/
export async function createCliFixture(_globalKey: string): Promise<CliTestFixture> {
const secretKey = process.env.STRIPE_SECRET_KEY;
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
const secretKey = env.E2E_STRIPE_SK;
const webhookSecret = env.E2E_STRIPE_WHSEC;
if (!secretKey || !webhookSecret) {
throw new Error("STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET must be set");
throw new Error("E2E_STRIPE_SK and E2E_STRIPE_WHSEC must be set");
}

const stripeClient = new Stripe(secretKey);

// Create a fresh test database
const dbName = `paykit_cli_${String(Date.now())}`;
const adminUrl = process.env.TEST_DATABASE_URL ?? "postgresql://localhost:5432/postgres";
const adminUrl = env.TEST_DATABASE_URL;
const adminPool = new Pool({ connectionString: adminUrl });
await adminPool.query(`CREATE DATABASE "${dbName}"`);
await adminPool.end();
Expand Down
25 changes: 25 additions & 0 deletions e2e/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import path from "node:path";

import { createEnv } from "@t3-oss/env-core";
import { config } from "dotenv";
import * as z from "zod";

config({ path: path.resolve(import.meta.dirname, "../.env"), quiet: true });
config({ path: path.resolve(import.meta.dirname, "../.env.local"), override: true, quiet: true });

export const env = createEnv({
server: {
PROVIDER: z.enum(["stripe", "polar"]).default("stripe"),
TEST_DATABASE_URL: z.string().default("postgresql://localhost:5432/postgres"),

// Stripe
E2E_STRIPE_SK: z.string().optional(),
E2E_STRIPE_WHSEC: z.string().optional(),

// Polar
E2E_POLAR_ACCESS_TOKEN: z.string().optional(),
E2E_POLAR_WHSEC: z.string().optional(),
},
runtimeEnv: process.env,
emptyStringAsUndefined: true,
});
8 changes: 7 additions & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"type": "module",
"scripts": {
"test": "vitest run --config smoke/vitest.automated.config.ts",
"test:stripe": "PROVIDER=stripe vitest run --config smoke/vitest.automated.config.ts",
"test:polar": "PROVIDER=polar vitest run --config smoke/vitest.automated.config.ts",
"test:watch": "vitest --config smoke/vitest.automated.config.ts",
"test:manual": "vitest run --config smoke/vitest.manual.config.ts",
"test:all": "vitest run --config smoke/vitest.config.ts",
Expand All @@ -12,13 +14,17 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"devDependencies": {
"@paykitjs/polar": "workspace:*",
"@paykitjs/stripe": "workspace:*",
"@t3-oss/env-core": "^0.12.0",
"@types/pg": "^8.18.0",
"dotenv": "^17.3.1",
"drizzle-orm": "^0.45.1",
"paykitjs": "workspace:*",
"pg": "^8.20.0",
"playwright": "^1.52.0",
"stripe": "^19.1.0",
"vitest": "^4.0.18"
"vitest": "^4.0.18",
"zod": "^4.0.0"
}
}
19 changes: 4 additions & 15 deletions e2e/smoke/cancel/cancel-then-upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
expectProduct,
expectSingleActivePlanInGroup,
expectSingleScheduledPlanInGroup,
subscribeCustomer,
type TestPayKit,
} from "../setup";

Expand All @@ -27,17 +28,9 @@ describe("cancel-then-upgrade: pro → free (scheduled) → ultra (upgrade)", ()
customerId = customer.customerId;

// Setup: subscribe to Pro, then schedule downgrade to Free
await t.paykit.subscribe({
customerId,
planId: "pro",
successUrl: "https://example.com/success",
});
await subscribeCustomer({ t, customerId, planId: "pro" });

await t.paykit.subscribe({
customerId,
planId: "free",
successUrl: "https://example.com/success",
});
await subscribeCustomer({ t, customerId, planId: "free" });
});

afterAll(async () => {
Expand Down Expand Up @@ -67,11 +60,7 @@ describe("cancel-then-upgrade: pro → free (scheduled) → ultra (upgrade)", ()
});

// Action: upgrade to Ultra
await t.paykit.subscribe({
customerId,
planId: "ultra",
successUrl: "https://example.com/success",
});
await subscribeCustomer({ t, customerId, planId: "ultra" });

// Ultra is active
await expectProduct({
Expand Down
25 changes: 5 additions & 20 deletions e2e/smoke/cancel/downgrade-change-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
expectProductNotPresent,
expectSingleActivePlanInGroup,
expectSingleScheduledPlanInGroup,
subscribeCustomer,
type TestPayKit,
} from "../setup";

Expand All @@ -28,24 +29,12 @@ describe("downgrade-change-target: ultra → pro (scheduled) → free (change ta
customerId = customer.customerId;

// Setup: subscribe Pro → upgrade Ultra
await t.paykit.subscribe({
customerId,
planId: "pro",
successUrl: "https://example.com/success",
});
await subscribeCustomer({ t, customerId, planId: "pro" });

await t.paykit.subscribe({
customerId,
planId: "ultra",
successUrl: "https://example.com/success",
});
await subscribeCustomer({ t, customerId, planId: "ultra" });

// Schedule downgrade to Pro
await t.paykit.subscribe({
customerId,
planId: "pro",
successUrl: "https://example.com/success",
});
await subscribeCustomer({ t, customerId, planId: "pro" });
});

afterAll(async () => {
Expand Down Expand Up @@ -75,11 +64,7 @@ describe("downgrade-change-target: ultra → pro (scheduled) → free (change ta
});

// Action: change downgrade target to Free instead
await t.paykit.subscribe({
customerId,
planId: "free",
successUrl: "https://example.com/success",
});
await subscribeCustomer({ t, customerId, planId: "free" });

// Ultra still canceling
await expectProduct({
Expand Down
7 changes: 2 additions & 5 deletions e2e/smoke/entitlements/check-boolean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createTestCustomerWithPM,
createTestPayKit,
dumpStateOnFailure,
subscribeCustomer,
type TestPayKit,
} from "../setup";

Expand Down Expand Up @@ -35,11 +36,7 @@ describe("check-boolean: boolean feature access", () => {
expect(freeCheck.allowed).toBe(false);

// Subscribe to Pro (includes dashboard)
await t.paykit.subscribe({
customerId,
planId: "pro",
successUrl: "https://example.com/success",
});
await subscribeCustomer({ t, customerId, planId: "pro" });

// On Pro — dashboard accessible
const proCheck = await t.paykit.check({ customerId, featureId: "dashboard" });
Expand Down
7 changes: 2 additions & 5 deletions e2e/smoke/entitlements/check-metered.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createTestPayKit,
dumpStateOnFailure,
expectExactMeteredBalance,
subscribeCustomer,
type TestPayKit,
} from "../setup";

Expand All @@ -25,11 +26,7 @@ describe("check-metered: metered feature balance and usage reporting", () => {
customerId = customer.customerId;

// Subscribe to Pro (500 messages/month)
await t.paykit.subscribe({
customerId,
planId: "pro",
successUrl: "https://example.com/success",
});
await subscribeCustomer({ t, customerId, planId: "pro" });
});

afterAll(async () => {
Expand Down
Loading