-
-
Notifications
You must be signed in to change notification settings - Fork 741
Supabase Storage and Stripe webhook examples #1345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
3d50d97
9bc830e
c998c60
20cc1fd
09b0c9f
e1ae353
ff87911
e52a272
f5d3429
0b2b961
619f2d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,150 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
--- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
title: "Trigger a task from Stripe webhook events" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
sidebarTitle: "Stripe webhook" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
description: "This example demonstrates how to handle Stripe webhook events using Trigger.dev." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
--- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## Overview | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This example shows how to set up a webhook handler for incoming Stripe events. The handler triggers a task when a `checkout.session.completed` event is received. This is easily customisable to handle other Stripe events. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## Key features | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- Shows how to create a Stripe webhook handler | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- Triggers a task when a `checkout.session.completed` event is received | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## Environment variables | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
You'll need to configure the following environment variables for this example to work: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- `STRIPE_WEBHOOK_SECRET` The secret key used to verify the Stripe webhook signature. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- `TRIGGER_API_URL` Your Trigger.dev API url: `https://api.trigger.dev` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- `TRIGGER_API_SECRET` Your Trigger.dev API secret. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## Setting up the Stripe webhook handler | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
First you'll need to create a [Stripe webhook](https://stripe.com/docs/webhooks) handler route that listens for POST requests and verifies the Stripe signature. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Here are examples of how you can set up a handler using different frameworks: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<CodeGroup> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
```ts Next.js | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// app/api/stripe-webhook/route.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { tasks } from "@trigger.dev/sdk/v3"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import Stripe from "stripe"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import type { stripeCheckoutCompleted } from "@/trigger/stripe-checkout-completed"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// 👆 **type-only** import | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export async function POST(request: Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const signature = request.headers.get("stripe-signature"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const payload = await request.text(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!signature || !payload) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ error: "Invalid Stripe payload/signature" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
status: 400, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const event = Stripe.webhooks.constructEvent( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
payload, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
signature, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
process.env.STRIPE_WEBHOOK_SECRET as string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Perform the check based on the event type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
switch (event.type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
case "checkout.session.completed": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Trigger the task only if the event type is "checkout.session.completed" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { id } = await tasks.trigger<typeof stripeCheckoutCompleted>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"stripe-checkout-completed", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
event | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json({ runId: id }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
default: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Return a response indicating that the event is not handled | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ message: "Event not handled" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
status: 200, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
```ts Remix | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// app/webhooks/stripe.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { type ActionFunctionArgs, json } from "@remix-run/node"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import type { stripeCheckoutCompleted } from "src/trigger/stripe-webhook"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { tasks } from "@trigger.dev/sdk/v3"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import Stripe from "stripe"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export async function action({ request }: ActionFunctionArgs) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Validate the Stripe webhook payload | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const signature = request.headers.get("stripe-signature"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const payload = await request.text(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!signature || !payload) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return json({ error: "Invalid Stripe payload/signature" }, { status: 400 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const event = Stripe.webhooks.constructEvent( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
payload, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
signature, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
process.env.STRIPE_WEBHOOK_SECRET as string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Perform the check based on the event type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
switch (event.type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
case "checkout.session.completed": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Trigger the task only if the event type is "checkout.session.completed" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { id } = await tasks.trigger<typeof stripeCheckoutCompleted>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"stripe-checkout-completed", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
event | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return json({ runId: id }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
default: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Return a response indicating that the event is not handled | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return json({ message: "Event not handled" }, { status: 200 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</CodeGroup> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## Task code | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This task is triggered when a `checkout.session.completed` event is received from Stripe. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
```ts trigger/stripe-webhook.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { task } from "@trigger.dev/sdk/v3"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import type stripe from "stripe"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const stripeCheckoutCompleted = task({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
id: "stripe-checkout-completed", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
run: async (payload: stripe.Event, {}) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Add your custom logic for handling the checkout.session.completed event here | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider enhancing the task code example The task code provides a good basic structure. To make it more informative, consider adding a more detailed example of what could be done in the Here's an example of how you might enhance the task code: export const stripeCheckoutCompleted = task({
id: "stripe-checkout-completed",
run: async (payload: stripe.Event, {}) => {
- // Add your custom logic for handling the checkout.session.completed event here
+ const session = payload.data.object as stripe.Checkout.Session;
+ console.log(`Processing completed checkout for session ${session.id}`);
+
+ // Example: Update order status in your database
+ // await updateOrderStatus(session.id, "paid");
+
+ // Example: Send a confirmation email to the customer
+ // await sendConfirmationEmail(session.customer_email);
},
}); Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## Testing your task locally | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
To test everything is working you can use the Stripe CLI to send test events to your endpoint: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1. Install the [Stripe CLI](https://stripe.com/docs/stripe-cli#install), and login | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2. Follow the instructions to [test your handler](https://docs.stripe.com/webhooks#test-webhook). This will include a temporary `STRIPE_WEBHOOK_SECRET` that you can use for testing. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3. When triggering the event, use the `checkout.session.completed` event type. With the Stripe CLI: `stripe trigger checkout.session.completed` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4. If your endpoint is set up correctly, you should see the Stripe events logged in your console with a status of `200`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
5. Then, check the [Trigger.dev](https://cloud.trigger.dev) dashboard and you should see the successful run of the `stripe-webhook` task. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
For more information on setting up and testing Stripe webhooks, refer to the [Stripe Webhook Documentation](https://stripe.com/docs/webhooks). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
--- | ||
title: "Upload a video to Supabase Storage using S3" | ||
sidebarTitle: "Supabase Storage upload" | ||
description: "This example demonstrates how to download a video from a URL and upload it to Supabase Storage using Trigger.dev." | ||
--- | ||
|
||
## Overview | ||
|
||
This task downloads a video from a provided URL, saves it to a temporary file, and then uploads the video file to Supabase Storage using S3. | ||
|
||
## Key features | ||
|
||
- Fetches a video from a provided URL | ||
- Uploads the video file to Supabase Storage | ||
|
||
## Task code | ||
|
||
```ts trigger/supabase-storage-upload.ts | ||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; | ||
import { logger, task } from "@trigger.dev/sdk/v3"; | ||
import fetch from "node-fetch"; | ||
|
||
// Initialize S3 client for Supabase Storage | ||
const s3Client = new S3Client({ | ||
region: process.env.SUPABASE_REGION, // Your Supabase project's region e.g. "us-east-1" | ||
endpoint: `https://${process.env.SUPABASE_PROJECT_ID}.supabase.co/storage/v1/s3`, | ||
credentials: { | ||
// These credentials can be found in your supabase storage settings, under 'S3 access keys' | ||
accessKeyId: process.env.SUPABASE_ACCESS_KEY_ID ?? "", | ||
secretAccessKey: process.env.SUPABASE_SECRET_ACCESS_KEY ?? "", | ||
}, | ||
}); | ||
|
||
export const supabaseStorageUpload = task({ | ||
id: "supabase-storage-upload", | ||
run: async (payload: { videoUrl: string }) => { | ||
const { videoUrl } = payload; | ||
|
||
// Fetch the video as an ArrayBuffer | ||
const response = await fetch(videoUrl); | ||
const videoArrayBuffer = await response.arrayBuffer(); | ||
const videoBuffer = Buffer.from(videoArrayBuffer); | ||
|
||
const bucket = "my_bucket"; // Replace "my_bucket" with your bucket name | ||
const objectKey = `video_${Date.now()}.mp4`; | ||
|
||
// Upload the video directly to Supabase Storage | ||
await s3Client.send( | ||
new PutObjectCommand({ | ||
Bucket: bucket, | ||
Key: objectKey, | ||
Body: videoBuffer, | ||
}) | ||
); | ||
logger.log(`Video uploaded to Supabase Storage bucket`, { objectKey }); | ||
|
||
// Return the video object key | ||
return { | ||
objectKey, | ||
bucket: bucket, | ||
}; | ||
}, | ||
}); | ||
``` | ||
|
||
## Testing your task | ||
|
||
To test this task in the dashboard, you can use the following payload: | ||
|
||
```json | ||
{ | ||
"videoUrl": "<a-video-url>" // Replace <a-video-url> with the URL of the video you want to upload | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improve consistency and error handling in code examples
The code examples for Next.js and Remix are well-structured, but there are some improvements that can be made:
Consistency in imports: In the Next.js example,
Stripe
is imported with a capital 'S', while in the Remix example, it'sstripe
in lowercase. Consider using consistent casing across examples.More specific error handling: The current error handling is good, but it could be more specific. Consider adding specific catch blocks for common errors, such as
Stripe.errors.StripeSignatureVerificationError
.Here's an example of how you might improve the error handling in both examples:
Apply similar changes to both the Next.js and Remix examples.
Tools
LanguageTool