Skip to content

Commit

Permalink
refactor: cleanup Stripe logic (denoland#525)
Browse files Browse the repository at this point in the history
This PR applies various fixes, cleanups and improvements to Stripe
logic.
  • Loading branch information
iuioiua authored Sep 7, 2023
1 parent c4c7a5f commit 5c47b8e
Show file tree
Hide file tree
Showing 15 changed files with 81 additions and 90 deletions.
9 changes: 4 additions & 5 deletions components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SITE_BAR_STYLES,
SITE_NAME,
} from "@/utils/constants.ts";
import { stripe } from "@/utils/payments.ts";
import { isStripeEnabled } from "@/utils/stripe.ts";
import IconX from "tabler_icons_tsx/x.tsx";
import IconMenu from "tabler_icons_tsx/menu-2.tsx";
import IconBell from "tabler_icons_tsx/bell.tsx";
Expand Down Expand Up @@ -76,8 +76,8 @@ export default function Header(
>
Dashboard
</a>
{stripe
? (
{isStripeEnabled() &&
(
<a
href="/pricing"
class={cx(
Expand All @@ -89,8 +89,7 @@ export default function Header(
>
Pricing
</a>
)
: null}
)}
{props.sessionUser
? (
<a
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"twind-preset-tailwind/": "https://esm.sh/@twind/[email protected]/",
"twind-preset-ext": "https://esm.sh/@twind/[email protected]/",
"std/": "https://raw.githubusercontent.com/lino-levan/deno_std/feat-ulid/",
"stripe": "./stripe.ts",
"stripe": "npm:/stripe@12.6.0",
"feed": "https://esm.sh/[email protected]",
"kv_oauth": "https://deno.land/x/[email protected]/mod.ts",
"tabler_icons_tsx/": "https://deno.land/x/[email protected]/tsx/",
Expand Down
9 changes: 5 additions & 4 deletions e2e_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,13 @@ Deno.test("[e2e] GET /blog", async () => {
});

Deno.test("[e2e] GET /pricing", async () => {
const resp = await handler(
new Request("http://localhost/pricing"),
);
const req = new Request("http://localhost/pricing");

Deno.env.delete("STRIPE_SECRET_KEY");
const resp = await handler(req);

assertFalse(resp.ok);
assertInstanceOf(resp.body, ReadableStream);
assertEquals(typeof await resp.text(), "string");
assertEquals(
resp.headers.get("content-type"),
"text/html; charset=utf-8",
Expand Down
7 changes: 7 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import config from "./fresh.config.ts";
import { isStripeEnabled } from "@/utils/stripe.ts";

console.log(
isStripeEnabled()
? "`STRIPE_SECRET_KEY` environment variable is defined. Stripe is enabled."
: "`STRIPE_SECRET_KEY` environment variable is not defined. Stripe is disabled.",
);

await start(manifest, config);
4 changes: 2 additions & 2 deletions plugins/kv_oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
updateUser,
type User,
} from "@/utils/db.ts";
import { stripe } from "@/utils/payments.ts";
import { isStripeEnabled, stripe } from "@/utils/stripe.ts";

const oauth2Client = createGitHubOAuth2Client();

Expand Down Expand Up @@ -55,7 +55,7 @@ export default {
const user = await getUser(githubUser.login);
if (!user) {
let stripeCustomerId = undefined;
if (stripe) {
if (isStripeEnabled()) {
const customer = await stripe.customers.create({
email: githubUser.email,
});
Expand Down
4 changes: 2 additions & 2 deletions routes/account/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { RouteContext } from "$fresh/server.ts";
import type { SignedInState } from "@/middleware/session.ts";
import { BUTTON_STYLES } from "@/utils/constants.ts";
import { ComponentChild } from "preact";
import { stripe } from "@/utils/payments.ts";
import { isStripeEnabled } from "@/utils/stripe.ts";
import Head from "@/components/Head.tsx";
import GitHubAvatarImg from "@/components/GitHubAvatarImg.tsx";

Expand Down Expand Up @@ -51,7 +51,7 @@ export default async function AccountPage(
title="Subscription"
text={sessionUser.isSubscribed ? "Premium 🦕" : "Free"}
>
{stripe && (
{isStripeEnabled() && (
<a
class="underline"
href={`/account/${action.toLowerCase()}`}
Expand Down
2 changes: 1 addition & 1 deletion routes/account/manage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import type { RouteContext } from "$fresh/server.ts";
import { stripe } from "@/utils/payments.ts";
import { stripe } from "@/utils/stripe.ts";
import type { SignedInState } from "@/middleware/session.ts";
import { redirect } from "@/utils/http.ts";
import { errors } from "std/http/http_errors.ts";
Expand Down
4 changes: 2 additions & 2 deletions routes/account/upgrade.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import type { RouteContext } from "$fresh/server.ts";
import { stripe } from "@/utils/payments.ts";
import { isStripeEnabled, stripe } from "@/utils/stripe.ts";
import type { SignedInState } from "@/middleware/session.ts";
import { redirect } from "@/utils/http.ts";
import { errors } from "std/http/http_errors.ts";
Expand All @@ -13,7 +13,7 @@ export default async function AccountUpgradePage(
_req: Request,
ctx: RouteContext<undefined, SignedInState>,
) {
if (stripe === undefined) throw new errors.NotFound();
if (!isStripeEnabled()) throw new errors.NotFound();
if (STRIPE_PREMIUM_PLAN_PRICE_ID === undefined) {
throw new Error(
'"STRIPE_PREMIUM_PLAN_PRICE_ID" environment variable not set',
Expand Down
8 changes: 4 additions & 4 deletions routes/api/stripe-webhooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { type Handlers, Status } from "$fresh/server.ts";
import { stripe } from "@/utils/payments.ts";
import { isStripeEnabled, stripe } from "@/utils/stripe.ts";
import Stripe from "stripe";
import { getUserByStripeCustomer, updateUser } from "@/utils/db.ts";
import { errors } from "std/http/http_errors.ts";
Expand All @@ -14,7 +14,7 @@ export const handler: Handlers = {
* 2. customer.subscription.deleted (when a user cancels the premium plan)
*/
async POST(req) {
if (stripe === undefined) throw new errors.NotFound();
if (!isStripeEnabled()) throw new errors.NotFound();

const body = await req.text();
const signature = req.headers.get("stripe-signature")!;
Expand All @@ -30,8 +30,8 @@ export const handler: Handlers = {
cryptoProvider,
);
} catch (error) {
console.error(error.message);
return new Response(error.message, { status: 400 });
console.error(error);
throw new errors.BadRequest();
}

// @ts-ignore: Property 'customer' actually does exist on type 'Object'
Expand Down
7 changes: 4 additions & 3 deletions routes/pricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import type { RouteContext } from "$fresh/server.ts";
import type { State } from "@/middleware/session.ts";
import { BUTTON_STYLES } from "@/utils/constants.ts";
import {
formatAmountForDisplay,
isProductWithPrice,
isStripeEnabled,
stripe,
StripProductWithPrice,
} from "@/utils/payments.ts";
} from "@/utils/stripe.ts";
import { formatAmountForDisplay } from "@/utils/display.ts";
import Stripe from "stripe";
import IconCheckCircle from "tabler_icons_tsx/circle-check.tsx";
import Head from "@/components/Head.tsx";
Expand Down Expand Up @@ -173,7 +174,7 @@ export default async function PricingPage(
_req: Request,
ctx: RouteContext<undefined, State>,
) {
if (stripe === undefined) return await ctx.renderNotFound();
if (!isStripeEnabled()) return await ctx.renderNotFound();

const { data } = await stripe.products.list({
expand: ["data.default_price"],
Expand Down
7 changes: 0 additions & 7 deletions stripe.ts

This file was deleted.

4 changes: 2 additions & 2 deletions tasks/init_stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type Stripe from "stripe";
import { SITE_DESCRIPTION } from "@/utils/constants.ts";
import "std/dotenv/load.ts";
import { stripe } from "@/utils/payments.ts";
import { isStripeEnabled, stripe } from "@/utils/stripe.ts";

async function createPremiumTierProduct(stripe: Stripe) {
/**
Expand Down Expand Up @@ -54,7 +54,7 @@ async function createDefaultPortalConfiguration(
}

async function main() {
if (stripe === undefined) throw new Error("Stripe is disabled.");
if (!isStripeEnabled()) throw new Error("Stripe is disabled.");

const product = await createPremiumTierProduct(stripe);

Expand Down
16 changes: 16 additions & 0 deletions utils/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ export function timeAgo(date: Date) {
// Remove the last character which is an "s"
return pluralize(amount, unit.slice(0, -1)) + " ago";
}

export function formatAmountForDisplay(
amount: number,
currency: string,
): string {
const numberFormat = new Intl.NumberFormat(
navigator.language,
{
style: "currency",
currency,
currencyDisplay: "symbol",
maximumFractionDigits: 0,
},
);
return numberFormat.format(amount);
}
57 changes: 0 additions & 57 deletions utils/payments.ts

This file was deleted.

31 changes: 31 additions & 0 deletions utils/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import Stripe from "stripe";
import "std/dotenv/load.ts";

const STRIPE_SECRET_KEY = Deno.env.get("STRIPE_SECRET_KEY");

export function isStripeEnabled() {
return Deno.env.has("STRIPE_SECRET_KEY");
}

export const stripe = new Stripe(STRIPE_SECRET_KEY!, {
apiVersion: "2022-11-15",
// Use the Fetch API instead of Node's HTTP client.
httpClient: Stripe.createFetchHttpClient(),
});

/**
* We assume that the product has a default price.
* The official types allow for the default_price to be `undefined | null | string`
*/
export type StripProductWithPrice = Stripe.Product & {
default_price: Stripe.Price;
};

export function isProductWithPrice(
product: Stripe.Product,
): product is StripProductWithPrice {
return product.default_price !== undefined &&
product.default_price !== null &&
typeof product.default_price !== "string";
}

0 comments on commit 5c47b8e

Please sign in to comment.