Skip to content
Open
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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<h3 align="center">The billing framework for TypeScript</h3>

<p align="center">
Define products in code. Any provider. Gate features. Track usage.
Define plans in code. Gate features. Track usage. Webhooks handled for you.
</p>

<p align="center">
Expand All @@ -37,7 +37,6 @@
PayKit is an embedded billing framework for TypeScript apps. It sits inside your app, uses your database, and gives you a single API to manage products, subscriptions, entitlements, and usage billing without touching provider dashboards.

```ts
import { stripe } from "@paykitjs/stripe";
import { createPayKit, feature, plan } from "paykitjs";

const messages = feature({ id: "messages", type: "metered" });
Expand All @@ -57,10 +56,10 @@ const pro = plan({
});

export const paykit = createPayKit({
provider: stripe({
stripe: {
secretKey: process.env.STRIPE_SECRET_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
}),
},
database: process.env.DATABASE_URL!,
products: [free, pro],
});
Expand Down
3 changes: 2 additions & 1 deletion apps/demo/drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import "dotenv/config";
export default defineConfig({
dialect: "postgresql",
schema: "../../packages/paykit/src/database/schema.ts",
out: "../../packages/paykit/src/database/migrations",
dbCredentials: {
url: process.env.POLAR_DATABASE_URL!,
url: process.env.PAYKIT_DATABASE_URL!,
},
migrations: {
schema: "public",
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";

/** @type {import("next").NextConfig} */
const config = {
transpilePackages: ["paykitjs", "@paykitjs/polar", "@paykitjs/stripe", "autumn-js"],
transpilePackages: ["paykitjs", "autumn-js"],
serverExternalPackages: ["pg"],
turbopack: {
root: fileURLToPath(new URL("../..", import.meta.url)),
Expand Down
7 changes: 2 additions & 5 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,14 @@
"tunnel": "set -a; [ -f .env ] && . ./.env; set +a; if [ -n \"$CF_TUNNEL_ID\" ]; then cloudflared tunnel --url http://localhost:3000 run \"$CF_TUNNEL_ID\"; fi",
"paykitjs": "bun ../../packages/paykit/src/cli/index.ts",
"typecheck": "tsc --noEmit",
"push": "bun push:auth && bun push:paykit:polar && bun push:paykit:stripe && bun push:autumn",
"push": "bun push:auth && bun push:paykit && bun push:autumn",
"push:auth": "bunx auth migrate --config src/lib/auth.ts --yes",
"push:paykit:polar": "set -a; [ -f .env ] && . ./.env; set +a; if [ -n \"$POLAR_DATABASE_URL\" ] && [ -n \"$POLAR_ACCESS_TOKEN\" ] && [ -n \"$POLAR_WEBHOOK_SECRET\" ]; then bunx paykitjs push --config paykit.polar.config.ts --yes; else printf '%s\\n' 'Skipping PayKit Polar push: provider env incomplete'; fi",
"push:paykit:stripe": "set -a; [ -f .env ] && . ./.env; set +a; if [ -n \"$STRIPE_DATABASE_URL\" ] && [ -n \"$STRIPE_SECRET_KEY\" ] && [ -n \"$STRIPE_WEBHOOK_SECRET\" ]; then bunx paykitjs push --config paykit.stripe.config.ts --yes; else printf '%s\\n' 'Skipping PayKit Stripe push: provider env incomplete'; fi",
"push:paykit": "set -a; [ -f .env ] && . ./.env; set +a; bunx paykitjs push --config paykit.config.ts --yes",
"push:autumn": "set -a; [ -f .env ] && . ./.env; set +a; if [ -n \"$AUTUMN_SECRET_KEY\" ]; then atmn push; else printf '%s\\n' 'Skipping Autumn push: provider env incomplete'; fi",
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"@base-ui/react": "^1.2.0",
"@paykitjs/polar": "workspace:*",
"@paykitjs/stripe": "workspace:*",
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.69.0",
"@trpc/client": "^11.0.0",
Expand Down
4 changes: 4 additions & 0 deletions apps/demo/paykit.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { paykit } from "./src/lib/paykit";

export { paykit };
export default paykit;
4 changes: 0 additions & 4 deletions apps/demo/paykit.polar.config.ts

This file was deleted.

4 changes: 0 additions & 4 deletions apps/demo/paykit.stripe.config.ts

This file was deleted.

20 changes: 4 additions & 16 deletions apps/demo/scripts/push-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,16 @@ async function main() {
env,
);

if (hasEnv(values, ["POLAR_DATABASE_URL", "POLAR_ACCESS_TOKEN", "POLAR_WEBHOOK_SECRET"])) {
console.log("Push PayKit Polar config");
if (hasEnv(values, ["PAYKIT_DATABASE_URL", "STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"])) {
console.log("Push PayKit config");
await runCommand(
"bunx",
["paykitjs", "push", "--config", "paykit.polar.config.ts", "--yes"],
["paykitjs", "push", "--config", "paykit.config.ts", "--yes"],
demoDir,
env,
);
} else {
console.log("Skipping PayKit Polar push: provider env incomplete");
}

if (hasEnv(values, ["STRIPE_DATABASE_URL", "STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"])) {
console.log("Push PayKit Stripe config");
await runCommand(
"bunx",
["paykitjs", "push", "--config", "paykit.stripe.config.ts", "--yes"],
demoDir,
env,
);
} else {
console.log("Skipping PayKit Stripe push: provider env incomplete");
console.log("Skipping PayKit push: env incomplete");
}

if (hasEnv(values, ["AUTUMN_SECRET_KEY"])) {
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/scripts/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const sandboxConfig = {
target: "production",
} as const;

export const paykitPackages = ["paykitjs", "@paykitjs/polar", "@paykitjs/stripe"] as const;
export const paykitPackages = ["paykitjs"] as const;

const scriptsDir = fileURLToPath(new URL(".", import.meta.url));

Expand Down
40 changes: 7 additions & 33 deletions apps/demo/src/app/_components/checkout-page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { authClient } from "@/lib/auth-client";
import { paykitScenarios } from "@/lib/paykit-scenarios";
import type { PayKitScenario } from "@/lib/paykit-scenarios";
import { api } from "@/trpc/react";

function PayKitTabContent({ scenario }: { scenario: PayKitScenario }) {
function PayKitTabContent() {
return (
<TabsContent value={`paykit-${scenario}`} className="flex flex-col gap-8 pt-4">
<SubscribePanel scenario={scenario} />
<TabsContent value="paykit" className="flex flex-col gap-8 pt-4">
<SubscribePanel />
<Separator />
<section className="flex flex-col gap-3">
<h2 className="text-sm font-medium">Features</h2>
<FeaturesPanel scenario={scenario} />
<FeaturesPanel />
</section>
</TabsContent>
);
Expand All @@ -37,14 +35,8 @@ export function CheckoutPageContent() {
const router = useRouter();
const toastShown = useRef(false);

const configuredPaykitScenarios = paykitScenarios.filter(
(scenario) => scenarios.data?.[scenario.id]?.configured,
);
const hasAutumn = scenarios.data?.autumn?.configured === true;
const availableTabs = [
...configuredPaykitScenarios.map((scenario) => scenario.tab),
...(hasAutumn ? ["autumn-stripe"] : []),
];
const availableTabs = ["paykit", ...(hasAutumn ? ["autumn-stripe"] : [])];
const tab = searchParams.get("tab");
const activeTab = tab && availableTabs.includes(tab) ? tab : availableTabs[0];
const setTab = useCallback(
Expand Down Expand Up @@ -109,18 +101,6 @@ export function CheckoutPageContent() {
);
}

if (!activeTab) {
return (
<div className="space-y-3">
<h1 className="text-2xl font-semibold tracking-tight">Billing</h1>
<p className="text-muted-foreground text-sm">
No billing providers are configured. Add a complete provider env group and restart the
demo.
</p>
</div>
);
}

return (
<div className="flex flex-col gap-8">
<div className="flex items-center justify-between">
Expand All @@ -146,16 +126,10 @@ export function CheckoutPageContent() {
<Separator />
<Tabs value={activeTab} onValueChange={setTab}>
<TabsList>
{configuredPaykitScenarios.map((scenario) => (
<TabsTrigger key={scenario.id} value={scenario.tab}>
{scenario.label}
</TabsTrigger>
))}
<TabsTrigger value="paykit">PayKit</TabsTrigger>
{hasAutumn ? <TabsTrigger value="autumn-stripe">Autumn Stripe</TabsTrigger> : null}
</TabsList>
{configuredPaykitScenarios.map((scenario) => (
<PayKitTabContent key={scenario.id} scenario={scenario.id} />
))}
<PayKitTabContent />
{hasAutumn ? (
<TabsContent value="autumn-stripe" className="flex flex-col gap-8 pt-4">
<AutumnSubscribePanel />
Expand Down
15 changes: 4 additions & 11 deletions apps/demo/src/app/_components/features-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,20 @@ import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { featureCatalog } from "@/lib/demo-catalog";
import type { PayKitScenario } from "@/lib/paykit-scenarios";
import { api } from "@/trpc/react";

function MeteredFeatureRow({
scenario,
featureId,
name,
description,
}: {
description: string;
featureId: string;
name: string;
scenario: PayKitScenario;
}) {
const utils = api.useUtils();
const paykitApi = scenario === "polar" ? api.paykitPolar : api.paykitStripe;
const paykitUtils = scenario === "polar" ? utils.paykitPolar : utils.paykitStripe;
const paykitApi = api.paykit;
const paykitUtils = utils.paykit;
const { data, isLoading } = paykitApi.checkFeature.useQuery({
featureId,
});
Expand Down Expand Up @@ -78,17 +75,15 @@ function MeteredFeatureRow({
}

function BooleanFeatureRow({
scenario,
featureId,
name,
description,
}: {
description: string;
featureId: string;
name: string;
scenario: PayKitScenario;
}) {
const paykitApi = scenario === "polar" ? api.paykitPolar : api.paykitStripe;
const paykitApi = api.paykit;
const { data, isLoading } = paykitApi.checkFeature.useQuery({
featureId,
});
Expand All @@ -110,22 +105,20 @@ function BooleanFeatureRow({
);
}

export function FeaturesPanel({ scenario }: { scenario: PayKitScenario }) {
export function FeaturesPanel() {
return (
<div className="flex flex-col gap-4">
{featureCatalog.map((feat) =>
feat.type === "metered" ? (
<MeteredFeatureRow
key={feat.id}
scenario={scenario}
featureId={feat.id}
name={feat.name}
description={feat.description}
/>
) : (
<BooleanFeatureRow
key={feat.id}
scenario={scenario}
featureId={feat.id}
name={feat.name}
description={feat.description}
Expand Down
Loading
Loading