From 849df0fcad0629435cef77c43117c680a1815707 Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Sun, 2 Jul 2023 10:13:43 +0300 Subject: [PATCH] fix: Stripe types and checks (#264) * throw error if not all products have a `default_price` * define stripe types in ./stripe.ts * fixes * tweak --------- Co-authored-by: Asher Gomez --- components/Layout.tsx | 2 +- deno.json | 2 +- routes/account/manage.ts | 4 +++- routes/api/stripe-webhooks.ts | 2 +- routes/pricing.tsx | 26 ++++++++++++++++++++------ stripe.ts | 7 +++++++ utils/payments.ts | 16 ++++++++++++++++ 7 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 stripe.ts diff --git a/components/Layout.tsx b/components/Layout.tsx index 80a9412d4..166c59592 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -6,7 +6,7 @@ import { SITE_WIDTH_STYLES, } from "@/utils/constants.ts"; import Logo from "./Logo.tsx"; -import { stripe } from "../utils/payments.ts"; +import { stripe } from "@/utils/payments.ts"; import { Discord, GitHub } from "./Icons.tsx"; interface NavProps extends JSX.HTMLAttributes { diff --git a/deno.json b/deno.json index 0a984aeec..dad8b4cb1 100644 --- a/deno.json +++ b/deno.json @@ -26,7 +26,7 @@ "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3", "twind-preset-tailwind/": "https://esm.sh/@twind/preset-tailwind@1.1.4/", "std/": "https://deno.land/std@0.188.0/", - "stripe": "https://esm.sh/stripe@12.6.0", + "stripe": "./stripe.ts", "feed": "https://esm.sh/feed@4.2.2", "fresh_charts/": "https://deno.land/x/fresh_charts@0.2.2/", "kv_oauth": "https://deno.land/x/deno_kv_oauth@v0.2.5/mod.ts", diff --git a/routes/account/manage.ts b/routes/account/manage.ts index 6e893f7e1..bebc2cce1 100644 --- a/routes/account/manage.ts +++ b/routes/account/manage.ts @@ -7,7 +7,9 @@ import { redirect } from "@/utils/redirect.ts"; // deno-lint-ignore no-explicit-any export const handler: Handlers = { async GET(req, ctx) { - if (stripe === undefined) return ctx.renderNotFound(); + if (stripe === undefined || ctx.state.user.stripeCustomerId === undefined) { + return ctx.renderNotFound(); + } const { url } = await stripe.billingPortal.sessions.create({ customer: ctx.state.user.stripeCustomerId, diff --git a/routes/api/stripe-webhooks.ts b/routes/api/stripe-webhooks.ts index c52390ddd..5cb4dafd6 100644 --- a/routes/api/stripe-webhooks.ts +++ b/routes/api/stripe-webhooks.ts @@ -1,7 +1,7 @@ // Copyright 2023 the Deno authors. All rights reserved. MIT license. import type { Handlers } from "$fresh/server.ts"; import { stripe } from "@/utils/payments.ts"; -import { Stripe } from "stripe"; +import Stripe from "stripe"; import { getUserByStripeCustomer, updateUser } from "@/utils/db.ts"; const cryptoProvider = Stripe.createSubtleCryptoProvider(); diff --git a/routes/pricing.tsx b/routes/pricing.tsx index 9f75e5427..9f57e327c 100644 --- a/routes/pricing.tsx +++ b/routes/pricing.tsx @@ -3,7 +3,12 @@ import type { Handlers, PageProps } from "$fresh/server.ts"; import Head from "@/components/Head.tsx"; import type { State } from "@/routes/_middleware.ts"; import { BUTTON_STYLES } from "@/utils/constants.ts"; -import { formatAmountForDisplay, stripe } from "@/utils/payments.ts"; +import { + formatAmountForDisplay, + isProductWithPrice, + stripe, + StripProductWithPrice, +} from "@/utils/payments.ts"; import Stripe from "stripe"; import { ComponentChild } from "preact"; import { getUserBySession, type User } from "@/utils/db.ts"; @@ -14,11 +19,11 @@ interface PricingPageData extends State { } function comparePrices( - productA: Stripe.Product, - productB: Stripe.Product, + productA: StripProductWithPrice, + productB: StripProductWithPrice, ) { - return ((productA.default_price as Stripe.Price).unit_amount || 0) - - ((productB.default_price as Stripe.Price).unit_amount || 0); + return (productA.default_price.unit_amount || 0) - + (productB.default_price.unit_amount || 0); } export const handler: Handlers = { @@ -29,7 +34,16 @@ export const handler: Handlers = { expand: ["data.default_price"], active: true, }); - const products = data.sort(comparePrices); + + const productsWithPrice = data.filter(isProductWithPrice); + + if (productsWithPrice.length !== data.length) { + throw new Error( + "Not all products have a default price. Please run the `deno task init:stripe` as the README instructs.", + ); + } + + const products = productsWithPrice.sort(comparePrices); const user = ctx.state.sessionId ? await getUserBySession(ctx.state.sessionId) diff --git a/stripe.ts b/stripe.ts new file mode 100644 index 000000000..a5bb980fd --- /dev/null +++ b/stripe.ts @@ -0,0 +1,7 @@ +// Copyright 2023 the Deno authors. All rights reserved. MIT license. + +// Default types for Stripe don't yet work: https://github.com/stripe-samples/stripe-node-deno-samples/issues/2 +// @deno-types="npm:stripe@12.6.0" +import Stripe from "https://esm.sh/stripe@12.6.0"; + +export default Stripe; diff --git a/utils/payments.ts b/utils/payments.ts index 61079f208..df81b3a32 100644 --- a/utils/payments.ts +++ b/utils/payments.ts @@ -24,6 +24,22 @@ if (stripe) { ); } +/** + * 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"; +} + export function formatAmountForDisplay( amount: number, currency: string,