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,