Skip to content

Commit 38774fe

Browse files
committed
License server: Add checkout tester tool
1 parent 13ad8f3 commit 38774fe

4 files changed

Lines changed: 226 additions & 2 deletions

File tree

apps/license-server/README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,38 @@ them to customers via Resend.
6767
- Test it with `4000 0566 5566 5556` / CVC: `100`, or one of the other test cards from
6868
https://developer.paddle.com/concepts/payment-methods/credit-debit-card#test-payment-details.
6969

70-
purchase URLs: https://sandbox-vendors.paddle.com/checkout/new?product_id=123456
70+
## Testing Paddle checkout
71+
72+
Test the full purchase flow through Paddle's sandbox. **This only works with sandbox credentials.**
73+
74+
### Prerequisites
75+
76+
1. **Set a default payment link** in Paddle sandbox:
77+
- Go to https://sandbox-vendors.paddle.com/checkout-settings
78+
- Under "Default payment link", enter `http://localhost:3333` (or any URL)
79+
- Save
80+
81+
2. **Create a client-side token**:
82+
- Go to https://sandbox-vendors.paddle.com/authentication-v2
83+
- Click "Client-side tokens" tab
84+
- Create a new token (will start with `test_`)
85+
86+
### Run the test
87+
88+
```bash
89+
# Get values from Paddle sandbox dashboard
90+
PADDLE_CLIENT_TOKEN=test_xxx PADDLE_PRICE_ID=pri_xxx pnpm test:checkout
91+
```
92+
93+
Then open http://localhost:3333 and click "Buy Cmdr".
94+
95+
### Troubleshooting
96+
97+
| Error | Fix |
98+
|-------|-----|
99+
| "Something went wrong" | Set default payment link in Paddle Checkout settings |
100+
| Token doesn't start with `test_` | Use sandbox token from sandbox-vendors.paddle.com |
101+
| "Invalid price" | Ensure price ID is from the same sandbox account |
71102

72103
## Endpoints
73104

apps/license-server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dev": "wrangler dev",
88
"deploy": "wrangler deploy",
99
"generate-keys": "node scripts/generate-keys.js",
10+
"test:checkout": "node test/server.js",
1011
"lint": "eslint .",
1112
"lint:fix": "eslint . --fix",
1213
"format": "prettier --write .",
@@ -33,4 +34,4 @@
3334
"vitest": "^4.0.16",
3435
"wrangler": "^4.58.0"
3536
}
36-
}
37+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Paddle Checkout Test</title>
7+
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
8+
<style>
9+
body {
10+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11+
max-width: 600px;
12+
margin: 100px auto;
13+
padding: 20px;
14+
text-align: center;
15+
}
16+
button {
17+
background: #5433FF;
18+
color: white;
19+
border: none;
20+
padding: 16px 32px;
21+
font-size: 18px;
22+
border-radius: 8px;
23+
cursor: pointer;
24+
margin: 10px;
25+
}
26+
button:hover {
27+
background: #4020DD;
28+
}
29+
.config {
30+
background: #f5f5f5;
31+
padding: 20px;
32+
border-radius: 8px;
33+
margin: 20px 0;
34+
text-align: left;
35+
font-family: monospace;
36+
font-size: 12px;
37+
}
38+
.test-cards {
39+
background: #e8f4e8;
40+
padding: 15px;
41+
border-radius: 8px;
42+
margin-top: 30px;
43+
text-align: left;
44+
}
45+
.test-cards h3 { margin-top: 0; }
46+
code { background: #ddd; padding: 2px 6px; border-radius: 3px; }
47+
</style>
48+
</head>
49+
<body>
50+
<h1>⌘ Cmdr Checkout Test</h1>
51+
<p>Test the Paddle checkout flow in sandbox environment.</p>
52+
53+
<div class="config" id="config">Loading config...</div>
54+
55+
<button id="buy">Buy Cmdr - $29</button>
56+
57+
<div class="test-cards">
58+
<h3>Test card details</h3>
59+
<p><strong>Card:</strong> <code>4000 0566 5566 5556</code></p>
60+
<p><strong>Expiry:</strong> Any future date (e.g., <code>12/30</code>)</p>
61+
<p><strong>CVC:</strong> <code>100</code></p>
62+
<p><a href="https://developer.paddle.com/concepts/payment-methods/credit-debit-card#test-payment-details" target="_blank">More test cards →</a></p>
63+
</div>
64+
65+
<script>
66+
// Config is injected by the server
67+
const config = window.PADDLE_CONFIG || {
68+
environment: 'sandbox',
69+
clientToken: 'MISSING_TOKEN',
70+
priceId: 'MISSING_PRICE_ID'
71+
};
72+
73+
// Show config
74+
document.getElementById('config').innerHTML = `
75+
<strong>Environment:</strong> ${config.environment}<br>
76+
<strong>Client Token:</strong> ${config.clientToken.slice(0, 20)}...<br>
77+
<strong>Price ID:</strong> ${config.priceId}
78+
`;
79+
80+
// Initialize Paddle
81+
if (config.environment === 'sandbox') {
82+
Paddle.Environment.set('sandbox');
83+
}
84+
Paddle.Initialize({ token: config.clientToken });
85+
86+
// Handle buy button
87+
document.getElementById('buy').addEventListener('click', () => {
88+
Paddle.Checkout.open({
89+
items: [{ priceId: config.priceId }]
90+
});
91+
});
92+
</script>
93+
</body>
94+
</html>

apps/license-server/test/server.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Tiny server to test Paddle checkout flow in SANDBOX environment only
2+
// Usage: PADDLE_CLIENT_TOKEN=test_xxx PADDLE_PRICE_ID=pri_xxx node server.js
3+
// Then open http://localhost:3333
4+
5+
import { readFileSync } from 'fs';
6+
import { createServer } from 'http';
7+
import { fileURLToPath } from 'url';
8+
import { dirname, join } from 'path';
9+
10+
const __dirname = dirname(fileURLToPath(import.meta.url));
11+
12+
const PORT = process.env.PORT || 3333;
13+
const PADDLE_CLIENT_TOKEN = process.env.PADDLE_CLIENT_TOKEN;
14+
const PADDLE_PRICE_ID = process.env.PADDLE_PRICE_ID;
15+
16+
// Validate inputs
17+
if (!PADDLE_CLIENT_TOKEN || !PADDLE_PRICE_ID) {
18+
console.error(`
19+
╭──────────────────────────────────────────────────────────╮
20+
│ Missing required environment variables │
21+
├──────────────────────────────────────────────────────────┤
22+
│ │
23+
│ PADDLE_CLIENT_TOKEN │
24+
│ → Paddle Sandbox > Developer Tools > Authentication │
25+
│ → Client-side tokens tab > Create new token │
26+
│ → Must start with "test_" for sandbox │
27+
│ │
28+
│ PADDLE_PRICE_ID │
29+
│ → Paddle Sandbox > Catalog > Products > Your product │
30+
│ → Click on a price > Copy the "pri_xxx" ID │
31+
│ │
32+
├──────────────────────────────────────────────────────────┤
33+
│ Example: │
34+
│ PADDLE_CLIENT_TOKEN=test_abc PADDLE_PRICE_ID=pri_01xxx │
35+
│ pnpm test:checkout │
36+
╰──────────────────────────────────────────────────────────╯
37+
`);
38+
process.exit(1);
39+
}
40+
41+
// Enforce sandbox-only
42+
if (!PADDLE_CLIENT_TOKEN.startsWith('test_')) {
43+
console.error(`
44+
╭──────────────────────────────────────────────────────────╮
45+
│ ERROR: This tester only works with sandbox tokens │
46+
├──────────────────────────────────────────────────────────┤
47+
│ │
48+
│ Your token "${PADDLE_CLIENT_TOKEN.slice(0, 10)}..." does not start with "test_"
49+
│ │
50+
│ Get a sandbox token from: │
51+
│ https://sandbox-vendors.paddle.com/authentication-v2 │
52+
│ │
53+
╰──────────────────────────────────────────────────────────╯
54+
`);
55+
process.exit(1);
56+
}
57+
58+
const html = readFileSync(join(__dirname, 'checkout.html'), 'utf-8');
59+
60+
// Always use sandbox environment
61+
const configScript = `<script>
62+
window.PADDLE_CONFIG = {
63+
environment: 'sandbox',
64+
clientToken: '${PADDLE_CLIENT_TOKEN}',
65+
priceId: '${PADDLE_PRICE_ID}'
66+
};
67+
</script>`;
68+
69+
const injectedHtml = html.replace('</head>', `${configScript}\n</head>`);
70+
71+
const server = createServer((req, res) => {
72+
res.writeHead(200, { 'Content-Type': 'text/html' });
73+
res.end(injectedHtml);
74+
});
75+
76+
server.listen(PORT, () => {
77+
console.log(`
78+
╭───────────────────────────────────────────────────────────────────╮
79+
│ Paddle Checkout Test Server (SANDBOX ONLY) │
80+
├───────────────────────────────────────────────────────────────────┤
81+
│ Environment: sandbox │
82+
│ Token: ${PADDLE_CLIENT_TOKEN.slice(0, 20).padEnd(46)}
83+
│ Price ID: ${PADDLE_PRICE_ID.padEnd(29)}
84+
│ │
85+
│ ➜ Open: http://localhost:${String(PORT).padEnd(26)}
86+
│ │
87+
│ Test card: 4000 0566 5566 5556 │
88+
│ Expiry: Any future date (e.g., 12/30) │
89+
│ CVC: 100 │
90+
├───────────────────────────────────────────────────────────────────┤
91+
│ ⚠️ If you see "Something went wrong", you need to: │
92+
│ │
93+
│ 1. Go to: https://sandbox-vendors.paddle.com/checkout-settings │
94+
│ 2. Set a "Default payment link" (can be localhost) │
95+
│ 3. Save and try again │
96+
╰───────────────────────────────────────────────────────────────────╯
97+
`);
98+
});

0 commit comments

Comments
 (0)