Skip to content

Commit 4e6c287

Browse files
Add Stripe store flow for free and paid digital products (#28)
* Add Stripe-powered store with free and paid digital products Co-authored-by: Amanda Nelson <jobs@amanda-nelson.com> * Enable runtime mode for Stripe store API routes Co-authored-by: Amanda Nelson <jobs@amanda-nelson.com> * Add Stripe webhook fulfillment and gated paid downloads Co-authored-by: Amanda Nelson <jobs@amanda-nelson.com> * Add Stripe CLI webhook testing docs for store Co-authored-by: Amanda Nelson <jobs@amanda-nelson.com> * update next and mdx * node updates and store testing * update jest config for ci * test * tootallnate update * netlify test --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 9d529b5 commit 4e6c287

29 files changed

Lines changed: 2235 additions & 894 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Set up Node.js
1717
uses: actions/setup-node@v3
1818
with:
19-
node-version: '18'
19+
node-version: '20'
2020
cache: 'npm'
2121

2222
- name: Install dependencies
@@ -42,7 +42,7 @@ jobs:
4242
- name: Set up Node.js
4343
uses: actions/setup-node@v3
4444
with:
45-
node-version: '18'
45+
node-version: '20'
4646
cache: 'npm'
4747

4848
- name: Install dependencies

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,8 +340,59 @@ A GitHub Actions workflow runs on all PRs and pushes to the main branch:
340340

341341
The project uses the following environment variables:
342342
- `NEXT_PUBLIC_HOTJAR_ID` - Hotjar tracking ID
343+
- `NEXT_PUBLIC_SITE_URL` - Canonical site URL used for store checkout redirects (example: `https://pythonessprogrammer.com`)
344+
- `STRIPE_SECRET_KEY` - Stripe secret key for paid product checkout sessions
345+
- `STRIPE_WEBHOOK_SECRET` - Stripe webhook signing secret for `POST /api/store/webhook`
346+
- `BEEHIIV_API_KEY` - Beehiiv API key for optional subscriber sync after free claims and paid purchases
347+
- `BEEHIIV_PUBLICATION_ID` - Beehiiv publication ID (format: `pub_...`) used by subscription API calls
348+
- `RESEND_API_KEY` - Resend API key for transactional purchase emails
349+
- `STORE_EMAIL_FROM` - Optional sender for transactional emails (default: `store@pythonessprogrammer.com`)
350+
- `STORE_DOWNLOAD_TOKEN_SECRET` - Optional HMAC secret for paid download gate tokens (falls back to `STRIPE_WEBHOOK_SECRET`)
343351
- Additional environment variables can be added in `.env.local`
344352

353+
## Stripe webhook local testing (small CLI flow)
354+
355+
Use Stripe CLI to test paid fulfillment locally (webhook + gated download + transactional email send).
356+
357+
1. Start the app:
358+
359+
```bash
360+
npm run dev
361+
```
362+
363+
2. In a second terminal, authenticate Stripe CLI if needed:
364+
365+
```bash
366+
stripe login
367+
```
368+
369+
3. Start webhook forwarding to the local webhook route:
370+
371+
```bash
372+
stripe listen --events checkout.session.completed --forward-to localhost:3000/api/store/webhook
373+
```
374+
375+
4. Copy the webhook signing secret shown by Stripe CLI (`whsec_...`) into `.env.local`:
376+
377+
```bash
378+
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxx
379+
```
380+
381+
5. Trigger a real test checkout in the app:
382+
- Open `http://localhost:3000/store`
383+
- Buy the paid product with a Stripe test card (for example `4242 4242 4242 4242`)
384+
- Confirm the redirect lands on `/store/success`
385+
386+
6. Verify fulfillment:
387+
- Stripe CLI shows `checkout.session.completed` delivered
388+
- webhook logs show `POST /api/store/webhook` succeeded
389+
- buyer receives transactional email if `RESEND_API_KEY` is configured
390+
- success-page download button and emailed link both pass through `/api/store/paid-download`
391+
392+
Notes:
393+
- `stripe trigger checkout.session.completed` can test webhook wiring, but a real checkout is the best end-to-end test because it includes your exact metadata and redirect behavior.
394+
- Free-product claims do not require Stripe and continue through `/api/store/free-claim`.
395+
345396
## Resources Page
346397

347398
The resources page (`/resources`) provides a comprehensive collection of free resources for digital wellness, automation, and neurodivergent-friendly tech solutions.

eslint.config.mjs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
1-
import { dirname } from "path";
2-
import { fileURLToPath } from "url";
3-
import { FlatCompat } from "@eslint/eslintrc";
4-
5-
const __filename = fileURLToPath(import.meta.url);
6-
const __dirname = dirname(__filename);
7-
8-
const compat = new FlatCompat({
9-
baseDirectory: __dirname,
10-
});
1+
import coreWebVitals from "eslint-config-next/core-web-vitals";
2+
import typescript from "eslint-config-next/typescript";
113

124
const eslintConfig = [
13-
...compat.extends("next/core-web-vitals", "next/typescript"),
5+
{
6+
ignores: [
7+
"node_modules/**",
8+
".next/**",
9+
"out/**",
10+
"coverage/**",
11+
"build/**",
12+
"public/**",
13+
],
14+
},
15+
...coreWebVitals,
16+
...typescript,
17+
{
18+
files: ["**/*.{ts,tsx}"],
19+
rules: {
20+
"@typescript-eslint/no-explicit-any": "warn",
21+
},
22+
},
23+
{
24+
files: [
25+
"**/*.config.js",
26+
"**/*.config.mjs",
27+
"**/*.config.ts",
28+
"jest.config.js",
29+
"jest.setup.js",
30+
"postcss.config.js",
31+
"scripts/**/*.js",
32+
],
33+
rules: {
34+
"@typescript-eslint/no-require-imports": "off",
35+
},
36+
},
1437
];
1538

1639
export default eslintConfig;

jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ const createJestConfig = nextJest({
99
const customJestConfig = {
1010
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
1111
testEnvironment: 'jest-environment-jsdom',
12+
// Non-http(s) origins are "opaque"; localStorage throws in CI without a real URL.
13+
testEnvironmentOptions: {
14+
url: 'http://localhost/',
15+
},
1216
moduleNameMapper: {
1317
// Handle module aliases
1418
'^@/(.*)$': '<rootDir>/src/$1',

netlify.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
[build]
22
command = "next build"
3-
publish = "out"
3+
# Overrides Netlify UI if it still points at `out` from the old static-export setup.
4+
# Standard `next build` writes to `.next/`; there is no `out/` without `output: "export"`.
5+
publish = ".next"
46

57
[build.environment]
6-
NODE_VERSION = "18"
8+
NODE_VERSION = "20"
79

810
[[plugins]]
911
package = "@netlify/plugin-nextjs"

next.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
3-
output: 'export',
3+
// Ensures next-mdx-remote uses the app’s React/jsx-runtime (avoids “older version of React” during prerender with Turbopack).
4+
transpilePackages: ['next-mdx-remote'],
45
reactStrictMode: true,
56
images: {
67
unoptimized: true,

0 commit comments

Comments
 (0)