Skip to content

Commit

Permalink
feat: make Stripe optional (#302)
Browse files Browse the repository at this point in the history
* feat: make Stripe optional

* work

* fix

* fixes and tweaks
  • Loading branch information
iuioiua authored Jun 27, 2023
1 parent 0b05713 commit 5a75926
Show file tree
Hide file tree
Showing 11 changed files with 66 additions and 34 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 8 additions & 4 deletions components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -87,10 +88,6 @@ interface LayoutProps {

export default function Layout(props: LayoutProps) {
const headerNavItems = [
{
href: "/pricing",
inner: "Pricing",
},
props.session
? {
href: "/account",
Expand All @@ -106,6 +103,13 @@ export default function Layout(props: LayoutProps) {
},
];

if (stripe !== undefined) {
headerNavItems.unshift({
href: "/pricing",
inner: "Pricing",
});
}

const footerNavItems = [
{
href: "/stats",
Expand Down
2 changes: 1 addition & 1 deletion routes/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Layout from "@/components/Layout.tsx";
export default function App({ Component, data }: AppProps) {
return (
<div>
<Layout session={data.sessionId}>
<Layout session={data?.sessionId}>
<Component />
</Layout>
</div>
Expand Down
15 changes: 9 additions & 6 deletions routes/account/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountState, AccountState> = {
GET(_request, ctx) {
Expand Down Expand Up @@ -55,12 +56,14 @@ export default function AccountPage(props: PageProps<AccountState>) {
title="Subscription"
text={props.data.user.isSubscribed ? "Premium 🦕" : "Free"}
>
<a
class="underline"
href={`/account/${action.toLowerCase()}`}
>
{action}
</a>
{stripe && (
<a
class="underline"
href={`/account/${action.toLowerCase()}`}
>
{action}
</a>
)}
</Row>
</ul>
<a
Expand Down
2 changes: 2 additions & 0 deletions routes/account/manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { redirect } from "@/utils/redirect.ts";
// deno-lint-ignore no-explicit-any
export const handler: Handlers<any, AccountState> = {
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",
Expand Down
5 changes: 4 additions & 1 deletion routes/account/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ const STRIPE_PREMIUM_PLAN_PRICE_ID = Deno.env.get(

export const handler: Handlers<null, AccountState> = {
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();
}

Expand Down
4 changes: 3 additions & 1 deletion routes/api/stripe-webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")!;
Expand Down
12 changes: 8 additions & 4 deletions routes/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,18 @@ export const handler: Handlers<any, State> = {

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(),
};
Expand Down
7 changes: 6 additions & 1 deletion routes/pricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PricingPageData, State> = {
async GET(_req, ctx) {
if (stripe === undefined) return ctx.renderNotFound();

const { data } = await stripe.products.list({
expand: ["data.default_price"],
active: true,
Expand Down
2 changes: 2 additions & 0 deletions tools/init_stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
34 changes: 19 additions & 15 deletions utils/payments.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down

0 comments on commit 5a75926

Please sign in to comment.