Skip to content

Commit 46ea216

Browse files
authored
Merge pull request #203 from getpaykit/feat/demo-refresh
Refresh demo and listen workflow
2 parents 0689e65 + 5fbd747 commit 46ea216

16 files changed

Lines changed: 432 additions & 51 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ These directories already contain source checkouts for some packages:
4444
- Never edit past migrations; create a new migration instead.
4545
- Never run "deploy" scripts to test anything, only if explicitly asked
4646
- NEVER EVER publish or release packages yourself, ask double confirmation
47+
- No need to pass --config paykit.ts into paykitjs CLI, because it's one of the locations resolved by default
48+
- If you just did some changes but later you see them different in the diff, it means the user likely adjusted the code intentionally, you should not restore them without asking the user

apps/demo/package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,15 @@
1010
"check:write": "biome check --write .",
1111
"deploy:sandbox": "bun scripts/deploy-sandbox.ts",
1212
"dev": "next dev",
13-
"dev:tunnel": "concurrently -k -n next,tunnel -c blue,magenta \"bun dev\" \"bun tunnel\"",
1413
"env:push:sandbox": "bun scripts/push-sandbox-env.ts",
1514
"preview": "next build && next start",
1615
"push:sandbox": "bun scripts/push-sandbox.ts",
1716
"start": "next start",
18-
"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",
1917
"paykitjs": "bun ../../packages/paykit/src/cli/index.ts",
2018
"typecheck": "tsc --noEmit",
21-
"push": "bun push:auth && bun push:paykit && bun push:autumn",
22-
"push:auth": "bunx auth migrate --config src/lib/auth.ts --yes",
23-
"push:paykit": "set -a; [ -f .env ] && . ./.env; set +a; bunx paykitjs push --config paykit.config.ts --yes",
19+
"push": "pnpm push:auth && pnpm push:paykit && pnpm push:autumn",
20+
"push:auth": "auth migrate --config src/lib/auth.ts --yes",
21+
"push:paykit": "pnpm paykitjs push --yes",
2422
"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",
2523
"db:studio": "drizzle-kit studio"
2624
},

apps/demo/paykit.config.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

apps/demo/scripts/deploy-sandbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88

99
function printHelp() {
1010
console.log("Deploy standalone demo app copy to Vercel sandbox production");
11-
console.log("Usage: bun deploy:sandbox --version <npm-spec>");
11+
console.log("Usage: pnpm deploy:sandbox --version <npm-spec>");
1212
}
1313

1414
async function main() {

apps/demo/scripts/push-sandbox.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,15 @@ async function main() {
2424

2525
console.log("Push auth migrations");
2626
await runCommand(
27-
"bunx",
28-
["auth", "migrate", "--config", "src/lib/auth.ts", "--yes"],
27+
"pnpm",
28+
["exec", "auth", "migrate", "--config", "src/lib/auth.ts", "--yes"],
2929
demoDir,
3030
env,
3131
);
3232

3333
if (hasEnv(values, ["PAYKIT_DATABASE_URL", "STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"])) {
3434
console.log("Push PayKit config");
35-
await runCommand(
36-
"bunx",
37-
["paykitjs", "push", "--config", "paykit.config.ts", "--yes"],
38-
demoDir,
39-
env,
40-
);
35+
await runCommand("pnpm", ["paykitjs", "push", "--yes"], demoDir, env);
4136
} else {
4237
console.log("Skipping PayKit push: env incomplete");
4338
}

apps/demo/scripts/sandbox.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,9 @@ async function rewriteTempNextConfig(nextConfigPath: string) {
201201

202202
async function writeTempVercelConfig(vercelConfigPath: string) {
203203
const vercelConfig = {
204-
buildCommand: "bun run build",
204+
buildCommand: "pnpm run build",
205205
framework: "nextjs",
206-
installCommand: "bun install",
206+
installCommand: "pnpm install",
207207
};
208208

209209
await writeFile(vercelConfigPath, `${JSON.stringify(vercelConfig, null, 2)}\n`);

apps/demo/src/app/_components/auth-form.tsx

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,69 @@
22

33
import { useRouter } from "next/navigation";
44
import { useState } from "react";
5+
import { toast } from "sonner";
56

67
import { Button } from "@/components/ui/button";
78
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
89
import { Input } from "@/components/ui/input";
910
import { Label } from "@/components/ui/label";
1011
import { authClient } from "@/lib/auth-client";
1112

13+
function createRandomCredentials() {
14+
const id = crypto.randomUUID().slice(0, 8);
15+
const email = `test-${Date.now()}-${id}@example.com`;
16+
const password = `PayKit-test-${crypto.randomUUID()}`;
17+
18+
return { email, password };
19+
}
20+
21+
function formatCredentials(credentials: { email: string; password: string }) {
22+
return `Email: ${credentials.email}\nPassword: ${credentials.password}`;
23+
}
24+
25+
async function copyCredentials(credentials: { email: string; password: string }) {
26+
const text = formatCredentials(credentials);
27+
28+
if (navigator.clipboard) {
29+
try {
30+
await navigator.clipboard.writeText(text);
31+
return;
32+
} catch {
33+
// Fall back to the textarea path when clipboard permissions reject writes.
34+
}
35+
}
36+
37+
const textarea = document.createElement("textarea");
38+
textarea.value = text;
39+
textarea.style.position = "fixed";
40+
textarea.style.opacity = "0";
41+
document.body.append(textarea);
42+
textarea.select();
43+
44+
try {
45+
if (!document.execCommand("copy")) {
46+
throw new Error("Clipboard copy failed");
47+
}
48+
} finally {
49+
textarea.remove();
50+
}
51+
}
52+
1253
export function AuthForm({ redirectTo }: { redirectTo: string }) {
1354
const router = useRouter();
1455
const [email, setEmail] = useState("");
1556
const [password, setPassword] = useState("");
1657
const [isSignUp, setIsSignUp] = useState(false);
1758
const [error, setError] = useState("");
1859
const [loading, setLoading] = useState(false);
60+
const [randomAccountLoading, setRandomAccountLoading] = useState(false);
1961

2062
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
2163
event.preventDefault();
64+
if (loading || randomAccountLoading) {
65+
return;
66+
}
67+
2268
setError("");
2369
setLoading(true);
2470

@@ -49,6 +95,65 @@ export function AuthForm({ redirectTo }: { redirectTo: string }) {
4995
}
5096
}
5197

98+
async function handleRandomAccount() {
99+
if (loading || randomAccountLoading) {
100+
return;
101+
}
102+
103+
setError("");
104+
setRandomAccountLoading(true);
105+
106+
const credentials = createRandomCredentials();
107+
let credentialsCopied = false;
108+
109+
try {
110+
setEmail(credentials.email);
111+
setPassword(credentials.password);
112+
113+
try {
114+
await copyCredentials(credentials);
115+
credentialsCopied = true;
116+
} catch {
117+
credentialsCopied = false;
118+
}
119+
120+
const result = await authClient.signUp.email({
121+
email: credentials.email,
122+
password: credentials.password,
123+
name: "Demo User",
124+
});
125+
126+
if (result.error) {
127+
const message = result.error.message ?? "Random account sign up failed";
128+
setError(message);
129+
toast.error(message, {
130+
description: credentialsCopied
131+
? "Generated credentials were copied, but the account was not created."
132+
: undefined,
133+
});
134+
return;
135+
}
136+
137+
if (credentialsCopied) {
138+
toast.success("Signed up with a random test account", {
139+
description: "Credentials copied to clipboard.",
140+
});
141+
} else {
142+
toast.warning("Signed up with a random test account", {
143+
description: `Clipboard copy failed. ${formatCredentials(credentials)}`,
144+
});
145+
}
146+
147+
router.replace(redirectTo);
148+
} catch (err) {
149+
const message = err instanceof Error ? err.message : "Something went wrong";
150+
setError(message);
151+
toast.error(message);
152+
} finally {
153+
setRandomAccountLoading(false);
154+
}
155+
}
156+
52157
return (
53158
<Card>
54159
<CardHeader>
@@ -80,9 +185,17 @@ export function AuthForm({ redirectTo }: { redirectTo: string }) {
80185
/>
81186
</div>
82187
{error ? <p className="text-destructive text-sm">{error}</p> : null}
83-
<Button disabled={loading} type="submit">
188+
<Button disabled={loading || randomAccountLoading} type="submit">
84189
{loading ? "Loading..." : isSignUp ? "Sign up" : "Sign in"}
85190
</Button>
191+
<Button
192+
disabled={loading || randomAccountLoading}
193+
onClick={handleRandomAccount}
194+
type="button"
195+
variant="secondary"
196+
>
197+
{randomAccountLoading ? "Creating account..." : "Sign up with random account"}
198+
</Button>
86199
<Button
87200
className="text-muted-foreground"
88201
onClick={() => {

apps/web/content/docs/cli.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,26 @@ paykitjs push -y && next build
4646

4747
This is similar to how you'd run database migrations on deploy. The `-y` flag skips the confirmation prompt.
4848

49+
## paykitjs listen
50+
51+
```bash
52+
npx paykitjs listen
53+
```
54+
55+
Runs PayKit's local webhook listener. It registers a provider webhook tunnel, replays missed events, and streams new webhooks while you develop.
56+
57+
Pass your app's dev command after `--` to run together with dev server:
58+
59+
```bash
60+
npx paykitjs listen -- pnpm dev
61+
```
62+
63+
Useful options:
64+
65+
- `--config <path>` loads a specific PayKit config file when default discovery is not enough
66+
- `--retry <window>` retries failed deliveries from a recent window, such as `5m`, `30s`, or `none`
67+
- `--forward-to <url>` forwards webhooks to a local app origin instead of applying them directly
68+
4969
## paykitjs status
5070

5171
```bash

apps/web/content/docs/webhook-events.mdx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ export const paykit = createPayKit({
6262

6363
The webhook endpoint is exposed by paykit handler at `/paykit/webhook` by default. See the [installation page](/docs/installation) for details on mounting it.
6464

65+
## Local development
66+
67+
Use `paykitjs listen` to receive provider webhooks while your app runs locally:
68+
69+
```bash
70+
pnpm paykitjs listen
71+
```
72+
73+
Or run with the web server together:
74+
75+
```bash
76+
pnpm paykitjs listen -- pnpm dev
77+
```
78+
6579
## Idempotency
6680

6781
PayKit records each incoming webhook event and processes it idempotently. If your provider sends the same event more than once, PayKit skips the duplicate. Your `on` handlers won't fire twice for the same event.

packages/paykit/package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,23 @@
2424
"types": "./src/index.ts",
2525
"exports": {
2626
".": {
27-
"paykit-source": "./src/index.ts",
2827
"types": "./src/index.ts",
28+
"paykit-source": "./src/index.ts",
2929
"default": "./src/index.ts"
3030
},
3131
"./handlers/next": {
32-
"paykit-source": "./src/handlers/next.ts",
3332
"types": "./src/handlers/next.ts",
33+
"paykit-source": "./src/handlers/next.ts",
3434
"default": "./src/handlers/next.ts"
3535
},
3636
"./client": {
37-
"paykit-source": "./src/client/index.ts",
3837
"types": "./src/client/index.ts",
38+
"paykit-source": "./src/client/index.ts",
3939
"default": "./src/client/index.ts"
4040
},
4141
"./database": {
42-
"paykit-source": "./src/database/schema.ts",
4342
"types": "./src/database/schema.ts",
43+
"paykit-source": "./src/database/schema.ts",
4444
"default": "./src/database/schema.ts"
4545
}
4646
},
@@ -51,10 +51,11 @@
5151
},
5252
"dependencies": {
5353
"@better-fetch/fetch": "^1.1.21",
54-
"@types/pg": "^8.18.0",
5554
"@clack/prompts": "^1.1.0",
55+
"@types/pg": "^8.18.0",
5656
"better-call": "^2.0.2",
5757
"commander": "^12.1.0",
58+
"concurrently": "^9.2.1",
5859
"dotenv": "^17.3.1",
5960
"drizzle-orm": "^0.45.1",
6061
"jiti": "^2.6.1",

0 commit comments

Comments
 (0)