diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..72f7bfc --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Polar.sh Integration +POLAR_ACCESS_TOKEN=your_polar_access_token_here +POLAR_30SEC_PRODUCT_ID=your_30sec_product_id_here +POLAR_60SEC_PRODUCT_ID=your_60sec_product_id_here +POLAR_SUCCESS_URL=https://whiskey.fm/sponsor/success + +# Optional: Set to "sandbox" for testing, "production" for live (defaults to production) +PUBLIC_POLAR_SERVER=production diff --git a/README.md b/README.md index 6d273c7..64b3e5e 100644 --- a/README.md +++ b/README.md @@ -110,3 +110,38 @@ see fit. We use Turso and Astro DB to setup guests per episode. If you would also like to do this, you will need a Turso account. + +## Polar.sh Checkout Integration + +This site uses Polar.sh for sponsor checkout. To set it up: + +1. **Get your Polar credentials:** + - Log in to your [Polar dashboard](https://polar.sh) + - Go to Settings → API to get your access token + - Create two products for your sponsorship packages (30-second and 60-second ads) + - Note the product IDs from each product's page + +2. **Configure environment variables:** + Create a `.env` file in the root directory with: + ```env + POLAR_ACCESS_TOKEN=your_polar_access_token_here + POLAR_30SEC_PRODUCT_ID=your_30sec_product_id_here + POLAR_60SEC_PRODUCT_ID=your_60sec_product_id_here + POLAR_SUCCESS_URL=https://whiskey.fm/sponsor/success + ``` + +3. **Test the integration:** + - For testing, you can set `PUBLIC_POLAR_SERVER=sandbox` in your `.env` + - Visit `/sponsor` and click on either sponsorship option + - You'll be redirected to Polar's checkout page + - After successful payment, users return to `/sponsor/success` + +4. **Go live:** + - Remove `PUBLIC_POLAR_SERVER` or set it to `production` + - Ensure your product IDs are for production products + - Test with a real payment to confirm everything works + +The integration uses the `@polar-sh/astro` package which provides: +- Server-side checkout session creation at `/api/checkout` +- Automatic tax compliance through Polar's Merchant of Record service +- Support for multiple products and dynamic pricing diff --git a/package.json b/package.json index 1c24257..b6175ff 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@astrojs/preact": "^4.1.1", "@astrojs/sitemap": "^3.6.0", "@astrojs/vercel": "^8.2.8", + "@polar-sh/astro": "^0.4.9", "@preact/signals": "^2.3.1", "@vercel/analytics": "^1.5.0", "@vercel/speed-insights": "^1.2.0", @@ -31,7 +32,8 @@ "preact": "^10.27.2", "rss-to-json": "^2.1.1", "schema-dts": "^1.1.5", - "valibot": "^0.31.1" + "valibot": "^0.31.1", + "zod": "^4.1.12" }, "devDependencies": { "@playwright/test": "^1.55.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8635df2..36b7685 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@astrojs/vercel': specifier: ^8.2.8 version: 8.2.8(astro@5.14.1(@types/node@22.18.7)(@vercel/functions@2.2.13)(jiti@2.6.0)(lightningcss@1.30.1)(rollup@4.52.3)(typescript@5.9.2)(yaml@2.8.1))(react@19.0.0)(rollup@4.52.3) + '@polar-sh/astro': + specifier: ^0.4.9 + version: 0.4.9(astro@5.14.1(@types/node@22.18.7)(@vercel/functions@2.2.13)(jiti@2.6.0)(lightningcss@1.30.1)(rollup@4.52.3)(typescript@5.9.2)(yaml@2.8.1)) '@preact/signals': specifier: ^2.3.1 version: 2.3.1(preact@10.27.2) @@ -53,6 +56,9 @@ importers: valibot: specifier: ^0.31.1 version: 0.31.1 + zod: + specifier: ^4.1.12 + version: 4.1.12 devDependencies: '@playwright/test': specifier: ^1.55.1 @@ -781,6 +787,24 @@ packages: engines: {node: '>=18'} hasBin: true + '@polar-sh/adapter-utils@0.2.8': + resolution: {integrity: sha512-VRCYectPSc8H63hYsGMNJK9rGy4WjpwL0G7LvnVHHYCpxBadkn90eWssS0AyaOsYaUiqCwwGcJd609UoDk3IQA==} + + '@polar-sh/astro@0.4.9': + resolution: {integrity: sha512-KOK/EUten6nv06LXolxMIrLnB+aPlVwXNSsYL0LD+svJp28lmKJo1GSnNhjhxZmoiX9TPPLL8n0C81pgOwGYjA==} + engines: {node: '>=16'} + peerDependencies: + astro: ^5.0.0 + + '@polar-sh/sdk@0.35.4': + resolution: {integrity: sha512-vv4Ptl5jNsHIZoLvzKr0wR+dGXJOpz8VWOOTEGqaiYx6YJvzIvrayg52qp8ZtBjsRBkySLZS2EVTV3wDHTACwA==} + hasBin: true + peerDependencies: + '@modelcontextprotocol/sdk': '>=1.5.0 <1.10.0' + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + '@preact/preset-vite@2.10.2': resolution: {integrity: sha512-K9wHlJOtkE+cGqlyQ5v9kL3Ge0Ql4LlIZjkUTL+1zf3nNdF88F9UZN6VTV8jdzBX9Fl7WSzeNMSDG7qECPmSmg==} peerDependencies: @@ -959,6 +983,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} @@ -1743,6 +1770,9 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -2737,6 +2767,9 @@ packages: resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} engines: {node: '>=16'} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + stream-replace-string@2.0.0: resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} @@ -3242,6 +3275,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -3974,6 +4010,25 @@ snapshots: dependencies: playwright: 1.55.1 + '@polar-sh/adapter-utils@0.2.8': + dependencies: + '@polar-sh/sdk': 0.35.4 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + + '@polar-sh/astro@0.4.9(astro@5.14.1(@types/node@22.18.7)(@vercel/functions@2.2.13)(jiti@2.6.0)(lightningcss@1.30.1)(rollup@4.52.3)(typescript@5.9.2)(yaml@2.8.1))': + dependencies: + '@polar-sh/adapter-utils': 0.2.8 + '@polar-sh/sdk': 0.35.4 + astro: 5.14.1(@types/node@22.18.7)(@vercel/functions@2.2.13)(jiti@2.6.0)(lightningcss@1.30.1)(rollup@4.52.3)(typescript@5.9.2)(yaml@2.8.1) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + + '@polar-sh/sdk@0.35.4': + dependencies: + standardwebhooks: 1.0.0 + zod: 3.25.76 + '@preact/preset-vite@2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@6.3.6(@types/node@22.18.7)(jiti@2.6.0)(lightningcss@1.30.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 @@ -4134,6 +4189,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@stablelib/base64@1.0.1': {} + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -4877,6 +4934,8 @@ snapshots: fast-json-stable-stringify@2.1.0: optional: true + fast-sha256@1.3.0: {} + fast-uri@3.1.0: {} fast-xml-parser@4.5.3: @@ -6099,6 +6158,11 @@ snapshots: stack-trace@1.0.0-pre2: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + stream-replace-string@2.0.0: {} string-width@4.2.3: @@ -6546,4 +6610,6 @@ snapshots: zod@3.25.76: {} + zod@4.1.12: {} + zwitch@2.0.4: {} diff --git a/src/components/AdPackageCard.astro b/src/components/AdPackageCard.astro index 893e125..43755a7 100644 --- a/src/components/AdPackageCard.astro +++ b/src/components/AdPackageCard.astro @@ -3,9 +3,10 @@ export interface Props { bullets: Array; heading: string; price: string; + productId: string; } -const { bullets, heading, price } = Astro.props; +const { bullets, heading, price, productId } = Astro.props; ---
@@ -37,7 +38,7 @@ const { bullets, heading, price } = Astro.props; diff --git a/src/pages/sponsor/success.astro b/src/pages/sponsor/success.astro new file mode 100644 index 0000000..eec681e --- /dev/null +++ b/src/pages/sponsor/success.astro @@ -0,0 +1,36 @@ +--- +import Layout from '../../layouts/Layout.astro'; +--- + + + +