diff --git a/README.md b/README.md
index 9c96b6f97..54b686fac 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,10 @@ Want to know where Deno SaaSKit is headed? Check out
3. Click `Generate a new client secret` and copy the resulting client secret to
the `GITHUB_CLIENT_SECRET` environment variable in your `.env` file.
-### Payments and Subscriptions (Stripe)
+### Payments and Subscriptions using Stripe (optional)
+
+> Note: Stripe is only enabled if the `STRIPE_SECRET_KEY` environment variable
+> is set.
1. Copy your Stripe secret key as `STRIPE_SECRET_KEY` into your `.env` file. We
recommend using the test key for your development environment.
diff --git a/components/Layout.tsx b/components/Layout.tsx
index 75f58c9ac..0fe0af868 100644
--- a/components/Layout.tsx
+++ b/components/Layout.tsx
@@ -7,6 +7,7 @@ import {
SITE_WIDTH_STYLES,
} from "@/utils/constants.ts";
import Logo from "./Logo.tsx";
+import { stripe } from "../utils/payments.ts";
function Notice() {
return (
@@ -87,10 +88,6 @@ interface LayoutProps {
export default function Layout(props: LayoutProps) {
const headerNavItems = [
- {
- href: "/pricing",
- inner: "Pricing",
- },
props.session
? {
href: "/account",
@@ -106,6 +103,13 @@ export default function Layout(props: LayoutProps) {
},
];
+ if (stripe !== undefined) {
+ headerNavItems.unshift({
+ href: "/pricing",
+ inner: "Pricing",
+ });
+ }
+
const footerNavItems = [
{
href: "/stats",
diff --git a/routes/_app.tsx b/routes/_app.tsx
index 08266d26b..988f0d513 100644
--- a/routes/_app.tsx
+++ b/routes/_app.tsx
@@ -5,7 +5,7 @@ import Layout from "@/components/Layout.tsx";
export default function App({ Component, data }: AppProps) {
return (
-
+
diff --git a/routes/account/index.tsx b/routes/account/index.tsx
index 658d9f960..ebc25223e 100644
--- a/routes/account/index.tsx
+++ b/routes/account/index.tsx
@@ -4,6 +4,7 @@ import Head from "@/components/Head.tsx";
import type { AccountState } from "./_middleware.ts";
import { BUTTON_STYLES } from "@/utils/constants.ts";
import { ComponentChild } from "preact";
+import { stripe } from "@/utils/payments.ts";
export const handler: Handlers = {
GET(_request, ctx) {
@@ -55,12 +56,14 @@ export default function AccountPage(props: PageProps) {
title="Subscription"
text={props.data.user.isSubscribed ? "Premium 🦕" : "Free"}
>
-
- {action}
-
+ {stripe && (
+
+ {action}
+
+ )}
= {
async GET(req, ctx) {
+ if (stripe === undefined) return ctx.renderNotFound();
+
const { url } = await stripe.billingPortal.sessions.create({
customer: ctx.state.user.stripeCustomerId,
return_url: new URL(req.url).origin + "/account",
diff --git a/routes/account/upgrade.ts b/routes/account/upgrade.ts
index bbc1ffee2..b07684983 100644
--- a/routes/account/upgrade.ts
+++ b/routes/account/upgrade.ts
@@ -10,7 +10,10 @@ const STRIPE_PREMIUM_PLAN_PRICE_ID = Deno.env.get(
export const handler: Handlers = {
async GET(req, ctx) {
- if (!STRIPE_PREMIUM_PLAN_PRICE_ID || !ctx.state.sessionId) {
+ if (
+ !STRIPE_PREMIUM_PLAN_PRICE_ID || !ctx.state.sessionId ||
+ stripe === undefined
+ ) {
return ctx.renderNotFound();
}
diff --git a/routes/api/stripe-webhooks.ts b/routes/api/stripe-webhooks.ts
index 2faf15b14..c52390ddd 100644
--- a/routes/api/stripe-webhooks.ts
+++ b/routes/api/stripe-webhooks.ts
@@ -12,7 +12,9 @@ export const handler: Handlers = {
* 1. customer.subscription.created (when a user subscribes to the premium plan)
* 2. customer.subscription.deleted (when a user cancels the premium plan)
*/
- async POST(req) {
+ async POST(req, ctx) {
+ if (stripe === undefined) return ctx.renderNotFound();
+
const body = await req.text();
const signature = req.headers.get("stripe-signature")!;
const signingSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET")!;
diff --git a/routes/callback.ts b/routes/callback.ts
index ed4a87bfb..a821fe6d3 100644
--- a/routes/callback.ts
+++ b/routes/callback.ts
@@ -51,14 +51,18 @@ export const handler: Handlers = {
const user = await getUser(githubUser.id.toString());
if (!user) {
- const customer = await stripe.customers.create({
- email: githubUser.email,
- });
+ let stripeCustomerId = undefined;
+ if (stripe) {
+ const customer = await stripe.customers.create({
+ email: githubUser.email,
+ });
+ stripeCustomerId = customer.id;
+ }
const user: User = {
id: githubUser.id.toString(),
login: githubUser.login,
avatarUrl: githubUser.avatar_url,
- stripeCustomerId: customer.id,
+ stripeCustomerId,
sessionId,
...newUserProps(),
};
diff --git a/routes/pricing.tsx b/routes/pricing.tsx
index 437401409..9f75e5427 100644
--- a/routes/pricing.tsx
+++ b/routes/pricing.tsx
@@ -13,13 +13,18 @@ interface PricingPageData extends State {
user: User | null;
}
-function comparePrices(productA: Stripe.Product, productB: Stripe.Product) {
+function comparePrices(
+ productA: Stripe.Product,
+ productB: Stripe.Product,
+) {
return ((productA.default_price as Stripe.Price).unit_amount || 0) -
((productB.default_price as Stripe.Price).unit_amount || 0);
}
export const handler: Handlers = {
async GET(_req, ctx) {
+ if (stripe === undefined) return ctx.renderNotFound();
+
const { data } = await stripe.products.list({
expand: ["data.default_price"],
active: true,
diff --git a/tools/init_stripe.ts b/tools/init_stripe.ts
index abd976aa4..4edd98f4f 100644
--- a/tools/init_stripe.ts
+++ b/tools/init_stripe.ts
@@ -55,6 +55,8 @@ async function createDefaultPortalConfiguration(
}
async function main() {
+ if (stripe === undefined) throw new Error("Stripe is disabled.");
+
const product = await createPremiumTierProduct(stripe);
if (typeof product.default_price !== "string") return;
diff --git a/utils/payments.ts b/utils/payments.ts
index a80d672d8..61079f208 100644
--- a/utils/payments.ts
+++ b/utils/payments.ts
@@ -1,25 +1,29 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import Stripe from "stripe";
-/** This constant allows preview deployments to successfully start up, making everything outside of the dashboard viewable. */
-const DUMMY_SECRET_KEY =
- "sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
+const STRIPE_SECRET_KEY = Deno.env.get("STRIPE_SECRET_KEY");
-if (!Deno.env.get("STRIPE_SECRET_KEY")) {
- console.warn(
- "`STRIPE_SECRET_KEY` environment variable is not defined. Dummy Stripe API key is currently in use. Stripe functionality is now limited.",
+export const stripe = STRIPE_SECRET_KEY !== undefined
+ ? new Stripe(
+ STRIPE_SECRET_KEY,
+ {
+ apiVersion: "2022-11-15",
+ // Use the Fetch API instead of Node's HTTP client.
+ httpClient: Stripe.createFetchHttpClient(),
+ },
+ )
+ : undefined;
+
+if (stripe) {
+ console.log(
+ "`STRIPE_SECRET_KEY` environment variable is defined. Stripe is enabled.",
+ );
+} else {
+ console.log(
+ "`STRIPE_SECRET_KEY` environment variable is not defined. Stripe is disabled.",
);
}
-export const stripe = new Stripe(
- Deno.env.get("STRIPE_SECRET_KEY") ?? DUMMY_SECRET_KEY,
- {
- apiVersion: "2022-11-15",
- // Use the Fetch API instead of Node's HTTP client.
- httpClient: Stripe.createFetchHttpClient(),
- },
-);
-
export function formatAmountForDisplay(
amount: number,
currency: string,