Skip to content

Commit

Permalink
Support for both live and test Stripe webhooks on a single deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
tjanczuk committed Mar 12, 2024
1 parent 23cbc78 commit 948e13b
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 26 deletions.
31 changes: 22 additions & 9 deletions apps/api/src/routes/stripe.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
import { Router } from "express";
import { RequestHandler, Router } from "express";
import { raw } from "body-parser";
import { validateWebhookEvent } from "@letsgo/stripe";
import { StripeMode, validateWebhookEvent } from "@letsgo/stripe";
import createError from "http-errors";
import { enqueue } from "@letsgo/queue";
import { MessageType } from "@letsgo/types";

const router = Router();

router.post(
"/webhook",
raw({ type: "application/json" }),
async (req, res, next) => {
const stripeHandler: (mode: StripeMode) => RequestHandler =
(mode: StripeMode) => async (req, res, next) => {
let event: any;
try {
event = await validateWebhookEvent({
body: req.body,
signature: req.headers["stripe-signature"] as string,
mode,
});
} catch (e: any) {
console.log("INVALID STRIPE WEBHOOK EVENT:", e.message);
console.log(`INVALID ${mode} STRIPE WEBHOOK EVENT:`, e.message);
next(createError(400, "Invalid Stripe webhook event"));
return;
}
await enqueue({
type: MessageType.Stripe,
payload: event,
payload: {
...event,
stripeMode: mode,
},
});
res.status(201).end();
}
};

router.post(
"/webhook/live",
raw({ type: "application/json" }),
stripeHandler("LIVE")
);

router.post(
"/webhook/test",
raw({ type: "application/json" }),
stripeHandler("TEST")
);

export default router;
6 changes: 4 additions & 2 deletions apps/worker/src/handlers/stripeHandlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { sendSlackMessage } from "@letsgo/slack";
import { MessageHandler } from "..";
import { invoicePaid } from "./invoicePaid";
import { StripeMode } from "@letsgo/stripe";

export interface StripeMessage<T> {
type: string;
data: {
object: T;
};
stripeMode: StripeMode;
[key: string]: any;
}

export const unrecognizedStripeMessageTypeHandler: MessageHandler<
StripeMessage<any>
> = async (message, event, context) => {
console.log(
"UNSUPPORTED STRIPE MESSAGE TYPE, IGNORING",
`UNSUPPORTED ${message.stripeMode} STRIPE MESSAGE TYPE, IGNORING`,
JSON.stringify(message, null, 2)
);
await sendSlackMessage(
`:credit_card: Worker received an unsupported Stripe webhook (ignoring): *${message.type}*`
`:credit_card: [${message.stripeMode}] Worker received an unsupported Stripe webhook (ignoring): *${message.type}*`
);
};

Expand Down
4 changes: 3 additions & 1 deletion apps/worker/src/handlers/stripeHandlers/invoicePaid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export const invoicePaid: MessageHandler<
const o = message.data.object;
await sendSlackMessage(
[
`:boom::moneybag: You've got paid *${o.currency.toUpperCase()} ${Math.round(
`:boom::moneybag: [${
message.stripeMode
}] You've got paid *${o.currency.toUpperCase()} ${Math.round(
o.amount_paid / 100
).toFixed(2)}*`,
`*Subscription Id:* ${o.subscription}`,
Expand Down
10 changes: 8 additions & 2 deletions docs/tutorials/setting-up-payments-with-stripe.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ We use Stripe CLI to tunnel webhook events related to payment and subscription l

### Collect the API keys from Stripe

For the purpose of this tutorial, we exclusively use Stripe's test mode intended for development and testing. Stripe's test mode does not affect the flow of actual money and has its own set of API keys separate from production. You will need three keys: _publishable key_, _secret key_, and the _webhook signing secret_.
For the purpose of this tutorial, we exclusively use Stripe's test mode intended for development and testing. Stripe's test mode does not affect the flow of actual money and has its own set of API keys separate from production. Setting up the live Stripe environment generally follows the same steps, except where indicated.

You will need three keys: _publishable key_, _secret key_, and the _webhook signing secret_.

You can access the _publishable key_ and _secret \_key_ for Stripe's test mode from [Test Mode API Keys](https://dashboard.stripe.com/test/apikeys) section of the Stripe dashboard.

Expand All @@ -28,7 +30,7 @@ To access the _webhook signing secret_, you must first register a new webhook en

1. Determine the base URL of your _API_ server running in AWS by executing `yarn ops status -a api`. The _Url_ property contains the _API_ base URL.
1. Go to the [Test Mode Webhooks](https://dashboard.stripe.com/test/webhooks) page of the Stripe's dashboard and choose _Add endpoint_ in the _Hosted endpoints_ section.
1. In the _Endpoint URL_, enter `{base-url}/v1/stripe/webhook`, where `{base-url}` is the _API_ base URL you determined above.
1. In the _Endpoint URL_, enter `{base-url}/v1/stripe/webhook/test`, where `{base-url}` is the _API_ base URL you determined above (**NOTE** when setting up live Stripe environment, you need to enter `{base-url}/v1/stripe/webhook/live` for the _Endpoint URL_).
1. Click _Select events_ and check the checkbox next to _select all events_. Then click _Add events_.
1. Back on the previous screen, click _Add endpoint_.
1. You will now see a page with the status of the endpoint. Click _Reveal_ under _Signing secret_. Take note of this _webhook signing secret_, its value starts with `whsec_`.
Expand Down Expand Up @@ -56,6 +58,8 @@ EOF

Remember to substitute the _publishable key_, _secret key_, and the _webhook signing secret_ obtained from the Stripe CLI above for `{publishable-key}`, `{secret-key}`, and `{cli-webhook-signing-secret}`, respectively.

**NOTE** When setting up live Stripe environment, you need to set the `LETSGO_STRIPE_LIVE_PUBLIC_KEY`, `LETSGO_STRIPE_LIVE_SECRET_KEY`, and `LETSGO_STRIPE_LIVE_WEBHOOK_KEY` configuration settings instead.

### Configure Stripe in AWS

Run the following commands to configure Stripe for the deployed version of your app in AWS:
Expand All @@ -69,6 +73,8 @@ yarn ops config set LETSGO_STRIPE_TEST_WEBHOOK_KEY={cloud-webhook-signing-secret

Remember to substitute the _publishable key_, _secret key_, and the _webhook signing secret_ obtained when adding a webhook endpoint in the Stripe dashboard for `{publishable-key}`, `{secret-key}`, and `{cloud-webhook-signing-secret}`, respectively. The `{cloud-webhook-signing-secret}` is the _webhook signing secret_ obtained when [registering a Stipe webook](#registering-a-stripe-webhook) previously.

**NOTE** When setting up live Stripe environment, you need to set the `LETSGO_STRIPE_LIVE_PUBLIC_KEY`, `LETSGO_STRIPE_LIVE_SECRET_KEY`, and `LETSGO_STRIPE_LIVE_WEBHOOK_KEY` configuration settings instead.

For those configuration changes to take effect, you need to re-deploy your application with:

```bash
Expand Down
34 changes: 22 additions & 12 deletions packages/stripe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,38 @@ import {
} from "@letsgo/constants";
import createError from "http-errors";

/**
* Stripe mode for the API client.
*/
export type StripeMode = "LIVE" | "TEST";

/**
* Configuration for _live_ or _test_ mode of the Stripe API client.
*/
export interface StripeConfiguration {
stripeMode: "LIVE" | "TEST";
stripeMode: StripeMode;
secretKey: string;
publicKey: string;
webhookKey: string;
}

function getStripeMode() {
function getStripeMode(): StripeMode {
const live = process.env["LETSGO_STRIPE_LIVE_MODE"] === "1";
const stripeMode = live ? "LIVE" : "TEST";
return stripeMode;
}

let stripeConfiguration: StripeConfiguration;
let stripeConfiguration: { [mode: string]: StripeConfiguration };
/**
* Determine the Stripe configuration based on the environment variables. Depending on the value of the `LETSGO_STRIPE_LIVE_MODE`
* environment variable, the configuration will use environment variables specific to the _live_ or _test_ mode to
* determine the public, secret, and webhook keys for Stripe.
* Determine the Stripe configuration based on the environment variables. If the _mode_ parameter is not specified,
* the mode is selected using the value of the `LETSGO_STRIPE_LIVE_MODE`
* environment variable. Based on the mode, the configuration will use environment variables specific to the
* _live_ or _test_ mode to determine the public, secret, and webhook keys for Stripe.
* @returns
*/
export function getStripeConfiguration(): StripeConfiguration {
if (!stripeConfiguration) {
const stripeMode = getStripeMode();
export function getStripeConfiguration(mode?: StripeMode): StripeConfiguration {
const stripeMode = mode || getStripeMode();
if (!stripeConfiguration[stripeMode]) {
const requiredEnvVars = [
"LETSGO_STRIPE_LIVE_MODE",
`LETSGO_STRIPE_${stripeMode}_SECRET_KEY`,
Expand All @@ -57,7 +63,7 @@ export function getStripeConfiguration(): StripeConfiguration {
)}.`
);
}
stripeConfiguration = {
stripeConfiguration[stripeMode] = {
stripeMode,
secretKey: process.env[
`LETSGO_STRIPE_${stripeMode}_SECRET_KEY`
Expand All @@ -70,7 +76,7 @@ export function getStripeConfiguration(): StripeConfiguration {
] as string,
};
}
return stripeConfiguration;
return stripeConfiguration[stripeMode];
}

let stripeClient: Stripe;
Expand All @@ -96,6 +102,10 @@ export interface ValidateWebhookEventOptions {
* The value of the `Stripe-signature` HTTP request header.
*/
signature: string;
/**
* The mode of the Stripe environment that sent the webhook event.
*/
mode: StripeMode;
}

/**
Expand All @@ -107,7 +117,7 @@ export async function validateWebhookEvent(
options: ValidateWebhookEventOptions
): Promise<Stripe.Event> {
const stripe = getStripeClient();
const webhookKey = getStripeConfiguration().webhookKey;
const webhookKey = getStripeConfiguration(options.mode).webhookKey;
const event = stripe.webhooks.constructEvent(
options.body,
options.signature,
Expand Down

0 comments on commit 948e13b

Please sign in to comment.