diff --git a/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-created.ts b/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-created.ts index 9d36b5ed0..e93cfe3f1 100644 --- a/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-created.ts +++ b/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-created.ts @@ -1,12 +1,13 @@ import {inngest} from 'inngest/inngest.server' import {v4} from 'uuid' import {prisma} from '@skillrecordings/database' -import {defaultContext as defaultStripeContext} from '@skillrecordings/stripe-sdk' import {loadSanityProduct} from './index' import {sanityWriteClient} from 'utils/sanity-server' import {SANITY_WEBHOOK_EVENT} from '../sanity-inngest-events' +import {paymentOptions} from 'pages/api/skill/[...skillRecordings]' +import {NonRetriableError} from 'inngest' -const {stripe} = defaultStripeContext +const stripe = paymentOptions.providers.stripe?.paymentClient export const sanityProductCreated = inngest.createFunction( {id: `product-create`, name: 'Create Product in Database'}, @@ -15,6 +16,10 @@ export const sanityProductCreated = inngest.createFunction( if: 'event.data.event == "product.create"', }, async ({event, step}) => { + if (!stripe) { + throw new NonRetriableError('Payment provider (Stripe) is missing') + } + const sanityProduct = await step.run('get sanity product', async () => { return loadSanityProduct(event.data._id) }) diff --git a/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-deleted.ts b/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-deleted.ts index b1d165756..7d24b8d3c 100644 --- a/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-deleted.ts +++ b/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-deleted.ts @@ -1,9 +1,10 @@ import {inngest} from 'inngest/inngest.server' import {prisma} from '@skillrecordings/database' import {SANITY_WEBHOOK_EVENT} from '../sanity-inngest-events' -import {defaultContext as defaultStripeContext} from '@skillrecordings/stripe-sdk' +import {paymentOptions} from 'pages/api/skill/[...skillRecordings]' +import {NonRetriableError} from 'inngest' -const {stripe} = defaultStripeContext +const stripe = paymentOptions.providers.stripe?.paymentClient export const sanityProductDeleted = inngest.createFunction( {id: `product-delete`, name: 'Deactivate Product in Database'}, @@ -12,6 +13,10 @@ export const sanityProductDeleted = inngest.createFunction( if: 'event.data.event == "product.delete"', }, async ({event, step}) => { + if (!stripe) { + throw new NonRetriableError('Payment provider (Stripe) is missing') + } + const {productId} = event.data if (!productId) { diff --git a/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-updated.ts b/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-updated.ts index 1240be456..d47ca1ea5 100644 --- a/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-updated.ts +++ b/apps/epic-web/src/inngest/functions/sanity/product/sanity-product-updated.ts @@ -3,9 +3,10 @@ import {prisma} from '@skillrecordings/database' import {v4} from 'uuid' import {loadSanityProduct} from './index' import {SANITY_WEBHOOK_EVENT} from '../sanity-inngest-events' -import {defaultContext as defaultStripeContext} from '@skillrecordings/stripe-sdk' +import {paymentOptions} from 'pages/api/skill/[...skillRecordings]' +import {NonRetriableError} from 'inngest' -const {stripe} = defaultStripeContext +const stripe = paymentOptions.providers.stripe?.paymentClient export const sanityProductUpdated = inngest.createFunction( { @@ -22,6 +23,10 @@ export const sanityProductUpdated = inngest.createFunction( if: 'event.data.event == "product.update"', }, async ({event, step}) => { + if (!stripe) { + throw new NonRetriableError('Payment provider (Stripe) is missing') + } + const sanityProduct = await step.run('get sanity product', async () => { return loadSanityProduct(event.data._id) }) diff --git a/apps/epic-web/src/inngest/functions/stripe/webhook-received.ts b/apps/epic-web/src/inngest/functions/stripe/webhook-received.ts index 9f3a1a8aa..c3d87d2f3 100644 --- a/apps/epic-web/src/inngest/functions/stripe/webhook-received.ts +++ b/apps/epic-web/src/inngest/functions/stripe/webhook-received.ts @@ -1,26 +1,24 @@ import {inngest} from 'inngest/inngest.server' import {STRIPE_WEBHOOK_RECEIVED_EVENT} from '@skillrecordings/inngest' -import {Redis} from '@upstash/redis' import {prisma} from '@skillrecordings/database' import {NonRetriableError} from 'inngest' import {postToSlack} from '@skillrecordings/skill-api' import {WebClient} from '@slack/web-api' -import { - defaultContext as defaultStripeContext, - Stripe, -} from '@skillrecordings/stripe-sdk' +import {paymentOptions} from 'pages/api/skill/[...skillRecordings]' +import {z} from 'zod' -const {stripe} = defaultStripeContext +const stripe = paymentOptions.providers.stripe?.paymentClient -const redis = new Redis({ - url: process.env.UPSTASH_REDIS_REST_URL!, - token: process.env.UPSTASH_REDIS_REST_TOKEN!, -}) +const CustomerSchema = z.object({id: z.string(), email: z.string()}) export const stripeWebhookReceived = inngest.createFunction( {id: `stripe-webhook-received`, name: 'Stripe Webhook Received'}, {event: STRIPE_WEBHOOK_RECEIVED_EVENT}, async ({event, step}) => { + if (!stripe) { + throw new NonRetriableError('Payment provider (Stripe) is missing') + } + const stripeAccountId = await step.run( 'get stripe account id', async () => { @@ -54,9 +52,9 @@ export const stripeWebhookReceived = inngest.createFunction( } const customer = await step.run('get customer', async () => { - return (await stripe.customers.retrieve( - invoice.customer as string, - )) as Stripe.Customer + return CustomerSchema.parse( + await stripe.customers.retrieve(invoice.customer as string), + ) }) if (!customer) { diff --git a/apps/epic-web/src/pages/api/skill/[...skillRecordings].ts b/apps/epic-web/src/pages/api/skill/[...skillRecordings].ts index 147f05bd1..871d842a6 100644 --- a/apps/epic-web/src/pages/api/skill/[...skillRecordings].ts +++ b/apps/epic-web/src/pages/api/skill/[...skillRecordings].ts @@ -6,6 +6,17 @@ import {nextAuthOptions} from '../auth/[...nextauth]' import {getToken} from 'next-auth/jwt' import {NextApiRequest} from 'next' import {getCurrentAbility, UserSchema} from '@skillrecordings/skill-lesson' +import { + defaultPaymentOptions, + StripeProvider, +} from '@skillrecordings/commerce-server' + +export const paymentOptions = defaultPaymentOptions({ + stripeProvider: StripeProvider({ + stripeSecretKey: process.env.STRIPE_SECRET_TOKEN, + apiVersion: '2020-08-27', + }), +}) export const skillOptions: SkillRecordingsOptions = { site: { @@ -13,6 +24,7 @@ export const skillOptions: SkillRecordingsOptions = { supportEmail: process.env.NEXT_PUBLIC_SUPPORT_EMAIL, }, nextAuthOptions, + paymentOptions, getAbility: async (req: IncomingRequest) => { const token = await getToken({req: req as unknown as NextApiRequest}) return getCurrentAbility({user: UserSchema.parse(token)}) diff --git a/apps/epic-web/src/pages/invoices/[merchantChargeId].tsx b/apps/epic-web/src/pages/invoices/[merchantChargeId].tsx index 9daf733fa..c44e1995b 100644 --- a/apps/epic-web/src/pages/invoices/[merchantChargeId].tsx +++ b/apps/epic-web/src/pages/invoices/[merchantChargeId].tsx @@ -1,10 +1,7 @@ import * as React from 'react' import {DownloadIcon} from '@heroicons/react/outline' -import {convertToSerializeForNextResponse} from '@skillrecordings/commerce-server' import {useLocalStorage} from 'react-use' import {GetServerSideProps} from 'next' -import {Coupon, MerchantProduct} from '@skillrecordings/database' -import {Stripe} from 'stripe' import fromUnixTime from 'date-fns/fromUnixTime' import Layout from 'components/app/layout' import format from 'date-fns/format' @@ -66,7 +63,9 @@ const Invoice: React.FC< const {charge, product, bulkCoupon, quantity} = chargeDetails.result - const customer = charge.customer as Stripe.Customer + const customer = z + .object({name: z.string().nullish(), email: z.string().nullish()}) + .parse(charge.customer) const formatUsd = (amount: number) => { return Intl.NumberFormat('en-US', { style: 'currency', diff --git a/apps/epic-web/src/pages/thanks/purchase.tsx b/apps/epic-web/src/pages/thanks/purchase.tsx index b0f323634..41cccb7ed 100644 --- a/apps/epic-web/src/pages/thanks/purchase.tsx +++ b/apps/epic-web/src/pages/thanks/purchase.tsx @@ -5,7 +5,6 @@ import { convertToSerializeForNextResponse, determinePurchaseType, PurchaseType, - stripeData, } from '@skillrecordings/commerce-server' import {getSdk, Purchase} from '@skillrecordings/database' import CopyInviteLink from '@skillrecordings/skill-lesson/team/copy-invite-link' @@ -17,32 +16,34 @@ import {isEmpty} from 'lodash' import {Transfer} from 'purchase-transfer/purchase-transfer' import {trpc} from 'trpc/trpc.client' import {getPostPurchaseThanksText} from 'utils/get-post-purchase-thanks-text' +import {paymentOptions} from 'pages/api/skill/[...skillRecordings]' export const getServerSideProps: GetServerSideProps = async (context) => { const {query} = context - const {session_id} = query + const session_id = + query.session_id instanceof Array ? query.session_id[0] : query.session_id - if (!session_id) { + const paymentProvider = paymentOptions.providers.stripe + + if (!session_id || !paymentProvider) { return { notFound: true, } } - const purchaseInfo = await stripeData({ - checkoutSessionId: session_id as string, - }) + const purchaseInfo = await paymentProvider.getPurchaseInfo(session_id) const { email, - stripeChargeId, + chargeIdentifier, quantity: seatsPurchased, - stripeProduct, + product: merchantProduct, } = purchaseInfo - const stripeProductName = stripeProduct.name + const stripeProductName = merchantProduct.name - const purchase = await getSdk().getPurchaseForStripeCharge(stripeChargeId) + const purchase = await getSdk().getPurchaseForStripeCharge(chargeIdentifier) if (!purchase || !email) { return { @@ -51,7 +52,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { } const purchaseType = await determinePurchaseType({ - checkoutSessionId: session_id as string, + checkoutSessionId: session_id, }) const product = await getProduct(purchase.productId) @@ -81,7 +82,6 @@ const InlineTeamInvite = ({ }) => { if (!bulkCouponId) return null - return (

Invite your team

diff --git a/apps/epic-web/src/pages/welcome/index.tsx b/apps/epic-web/src/pages/welcome/index.tsx index 9fec40066..a3b8e1bcb 100644 --- a/apps/epic-web/src/pages/welcome/index.tsx +++ b/apps/epic-web/src/pages/welcome/index.tsx @@ -1,40 +1,38 @@ import * as React from 'react' -import {DocumentTextIcon, UserGroupIcon} from '@heroicons/react/outline' -import { - convertToSerializeForNextResponse, - stripeData, -} from '@skillrecordings/commerce-server' import {useSession} from 'next-auth/react' import {GetServerSideProps} from 'next' import {getToken} from 'next-auth/jwt' import Layout from 'components/app/layout' import {getSdk, prisma} from '@skillrecordings/database' import Link from 'next/link' -import {first, isString} from 'lodash' +import {isString} from 'lodash' import InviteTeam from '@skillrecordings/skill-lesson/team' import {InvoiceCard} from 'pages/invoices' -import MuxPlayer from '@mux/mux-player-react' import {SanityDocument} from '@sanity/client' import Image from 'next/legacy/image' import {trpc} from '../../trpc/trpc.client' import {Transfer} from 'purchase-transfer/purchase-transfer' import {getProduct} from 'lib/products' +import {paymentOptions} from 'pages/api/skill/[...skillRecordings]' export const getServerSideProps: GetServerSideProps = async ({req, query}) => { - const {purchaseId: purchaseQueryParam, session_id, upgrade} = query + const {purchaseId: purchaseQueryParam, upgrade} = query + const session_id = + query.session_id instanceof Array ? query.session_id[0] : query.session_id const token = await getToken({req}) const {getPurchaseDetails} = getSdk() + const paymentProvider = paymentOptions.providers.stripe + let purchaseId = purchaseQueryParam - if (session_id) { - const {stripeChargeId} = await stripeData({ - checkoutSessionId: session_id as string, - }) + if (session_id && paymentProvider) { + const {chargeIdentifier} = await paymentProvider.getPurchaseInfo(session_id) + const purchase = await prisma.purchase.findFirst({ where: { merchantCharge: { - identifier: stripeChargeId, + identifier: chargeIdentifier, }, }, })