ReviewOS

also looking at this

stacks/uplink

feat(billing): issue the license from Stripe, not only from the thank-you page

#49
Merged glennmichael123 wants to merge feat/stripe-webhook into main
9 files +439 -2
app/Actions/Billing/StripeWebhookAction.tsadded+34-0
Changes to app/Actions/Billing/StripeWebhookAction.ts
@@ -0,0 +1,34 @@
1import type { RequestInstance } from '@stacksjs/types'
2import { Action } from '@stacksjs/actions'
3import { response } from '@stacksjs/router'
4import { licenses } from '../../Billing/license-store'
5import { bodyFor, receiveWebhook, statusFor, webhookSecret } from '../../Billing/webhook'
6
7/**
8 * POST /api/billing/webhook - Stripe's copy of what happened, so a license is
9 * issued even when the buyer never reaches the thank-you page (#48).
10 *
11 * `rawBody()` rather than the parsed body: Stripe signs the bytes it sent, and
12 * a re-serialized JSON body does not match the HMAC. The router keeps them for
13 * exactly this.
14 */
15export default new Action({
16 name: 'Stripe Webhook',
17 description: 'Issue a license from a signed Stripe checkout event',
18 method: 'POST',
19
20 async handle(request: RequestInstance) {
21 const outcome = await receiveWebhook(
22 await request.rawBody?.() ?? null,
23 request.headers.get('stripe-signature'),
24 { secret: webhookSecret(), issue: async sessionId => (await licenses()).issue(sessionId) },
25 )
26
27 // Stripe shows these in the dashboard, and they are the only trace of a
28 // purchase that did not become a license.
29 if (outcome.kind !== 'licensed')
30 console.error(`[stripe-webhook] ${outcome.kind}: ${JSON.stringify(bodyFor(outcome))}`)
31
32 return response.json(bodyFor(outcome), statusFor(outcome))
33 },
34})