mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-04-10 12:59:05 +02:00
stripe webhook sig & more
This commit is contained in:
parent
17cba4ec16
commit
af979e2cd0
@ -7,6 +7,8 @@
|
||||
STRIPE_KEY=
|
||||
# to create a test subscription, go to https://dashboard.stripe.com/test/products and click on + Add Product
|
||||
SUBSCRIPTION_PRICE_ID=
|
||||
# after starting the stripe cli with `stripe listen --forward-to localhost:3001/stripe-webhook` it will output your signing secret
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
|
||||
# see our guide for setting up google auth: https://wasp-lang.dev/docs/auth/social-auth/google
|
||||
GOOGLE_CLIENT_ID=
|
||||
|
20
main.wasp
20
main.wasp
@ -65,8 +65,6 @@ app SaaSTemplate {
|
||||
("@tailwindcss/typography", "^0.5.7"),
|
||||
("react-hook-form", "7.43.1"),
|
||||
("react-icons", "4.11.0"),
|
||||
("request-ip", "3.3.0"),
|
||||
("@types/request-ip", "0.0.37"),
|
||||
("node-fetch", "3.3.0"),
|
||||
("react-hook-form", "^7.45.4"),
|
||||
("stripe", "11.15.0"),
|
||||
@ -95,8 +93,9 @@ entity User {=psl
|
||||
stripeId String?
|
||||
checkoutSessionId String?
|
||||
hasPaid Boolean @default(false)
|
||||
sendEmail Boolean @default(false)
|
||||
subscriptionType String?
|
||||
subscriptionStatus String?
|
||||
sendEmail Boolean @default(false)
|
||||
datePaid DateTime?
|
||||
credits Int @default(3)
|
||||
relatedObject RelatedObject[]
|
||||
@ -143,8 +142,8 @@ entity DailyStats {=psl
|
||||
paidUserCount Int @default(0)
|
||||
userDelta Int @default(0)
|
||||
paidUserDelta Int @default(0)
|
||||
totalRevenue Int @default(0)
|
||||
totalProfit Int @default(0)
|
||||
totalRevenue Float @default(0)
|
||||
totalProfit Float @default(0)
|
||||
psl=}
|
||||
|
||||
entity Referrer {=psl
|
||||
@ -287,11 +286,6 @@ action stripePayment {
|
||||
entities: [User]
|
||||
}
|
||||
|
||||
// action stripeCreditsPayment {
|
||||
// fn: import { stripeCreditsPayment } from "@server/actions.js",
|
||||
// entities: [User]
|
||||
// }
|
||||
|
||||
action updateCurrentUser {
|
||||
fn: import { updateCurrentUser } from "@server/actions.js",
|
||||
entities: [User]
|
||||
@ -340,8 +334,9 @@ query getPaginatedUsers {
|
||||
*/
|
||||
|
||||
api stripeWebhook {
|
||||
fn: import { stripeWebhook } from "@server/webhooks.js",
|
||||
fn: import { stripeWebhook } from "@server/webhooks/stripe.js",
|
||||
entities: [User],
|
||||
middlewareConfigFn: import { stripeMiddlewareFn } from "@server/webhooks/stripe.js",
|
||||
httpRoute: (POST, "/stripe-webhook")
|
||||
}
|
||||
|
||||
@ -366,7 +361,8 @@ job dailyStats {
|
||||
fn: import { calculateDailyStats } from "@server/workers/calculateDailyStats.js"
|
||||
},
|
||||
schedule: {
|
||||
cron: "* * * * *" //
|
||||
// every hour
|
||||
cron: "0 * * * *"
|
||||
},
|
||||
entities: [User, DailyStats, Logs]
|
||||
}
|
||||
|
5
migrations/20231120125851_dailystats_float/migration.sql
Normal file
5
migrations/20231120125851_dailystats_float/migration.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "DailyStats" ALTER COLUMN "totalProfit" SET DEFAULT 0,
|
||||
ALTER COLUMN "totalProfit" SET DATA TYPE DOUBLE PRECISION,
|
||||
ALTER COLUMN "totalRevenue" SET DEFAULT 0,
|
||||
ALTER COLUMN "totalRevenue" SET DATA TYPE DOUBLE PRECISION;
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "subscriptionType" TEXT;
|
@ -54,6 +54,16 @@ export default function App({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}, [user, referrer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.hash) {
|
||||
const id = location.hash.replace('#', '');
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAdminDashboard ? (
|
||||
|
@ -12,7 +12,8 @@ const TotalRevenueCard = ({dailyStats, weeklyStats, isLoading}: DailyStatsProps)
|
||||
if ( !weeklyStats || isLoading) return;
|
||||
weeklyStats.sort((a, b) => b.id - a.id);
|
||||
console.log('weeklyStats[1]?.totalRevenue; ', !!weeklyStats && weeklyStats)
|
||||
return ((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / weeklyStats[1]?.totalRevenue) * 100;
|
||||
const percentage = ((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / weeklyStats[1]?.totalRevenue) * 100;
|
||||
return Math.floor(percentage);
|
||||
}, [weeklyStats]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -2,16 +2,15 @@ import { User } from '@wasp/entities';
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getRelatedObjects from '@wasp/queries/getRelatedObjects'
|
||||
import logout from '@wasp/auth/logout';
|
||||
import stripePayment from '@wasp/actions/stripePayment';
|
||||
import { useState, Dispatch, SetStateAction } from 'react';
|
||||
import { Link } from '@wasp/router'
|
||||
import { CUSTOMER_PORTAL_LINK } from '../const';
|
||||
|
||||
// get your own link from your stripe dashboard: https://dashboard.stripe.com/settings/billing/portal
|
||||
const CUSTOMER_PORTAL_LINK = 'https://billing.stripe.com/p/login/test_8wM8x17JN7DT4zC000';
|
||||
|
||||
export default function Example({ user }: { user: User }) {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const { data: relatedObjects, isLoading: isLoadingRelatedObjects } = useQuery(getRelatedObjects)
|
||||
const { data: relatedObjects, isLoading: isLoadingRelatedObjects } = useQuery(getRelatedObjects);
|
||||
|
||||
return (
|
||||
<div className='mt-10 px-6'>
|
||||
@ -69,26 +68,13 @@ export default function Example({ user }: { user: User }) {
|
||||
}
|
||||
|
||||
function BuyMoreButton({ isLoading, setIsLoading }: { isLoading: boolean, setIsLoading: Dispatch<SetStateAction<boolean>> }) {
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const stripeResults = await stripePayment();
|
||||
if (stripeResults?.sessionUrl) {
|
||||
window.open(stripeResults.sessionUrl, '_self');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
alert(error?.message ?? 'Something went wrong.')
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className='ml-4 flex-shrink-0 sm:col-span-1 sm:mt-0'>
|
||||
<button onClick={handleClick} className={`font-medium text-sm text-indigo-600 hover:text-indigo-500 ${isLoading && 'animate-pulse'}`}>
|
||||
<Link to='/' hash='pricing' className={`font-medium text-sm text-indigo-600 hover:text-indigo-500 ${isLoading && 'animate-pulse'}`}>
|
||||
{!isLoading ? 'Buy More/Upgrade' : 'Loading...'}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { User } from '@wasp/entities';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
export default function CheckoutPage({ user }: { user: User }) {
|
||||
const [hasPaid, setHasPaid] = useState('loading');
|
||||
export default function CheckoutPage() {
|
||||
const [paymentStatus, setPaymentStatus] = useState('loading');
|
||||
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
function delayedRedirect() {
|
||||
@ -14,14 +14,14 @@ export default function CheckoutPage({ user }: { user: User }) {
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const cancel = urlParams.get('canceled');
|
||||
const success = urlParams.get('success');
|
||||
const credits = urlParams.get('credits');
|
||||
if (cancel) {
|
||||
setHasPaid('canceled');
|
||||
} else if (success) {
|
||||
setHasPaid('paid');
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const isSuccess = queryParams.get('success')
|
||||
const isCanceled = queryParams.get('canceled')
|
||||
|
||||
if (isCanceled) {
|
||||
setPaymentStatus('canceled');
|
||||
} else if (isSuccess) {
|
||||
setPaymentStatus('paid');
|
||||
} else {
|
||||
history.push('/account');
|
||||
}
|
||||
@ -29,20 +29,22 @@ export default function CheckoutPage({ user }: { user: User }) {
|
||||
return () => {
|
||||
clearTimeout(delayedRedirect());
|
||||
};
|
||||
}, []);
|
||||
}, [location]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className='flex min-h-full flex-col justify-center mt-10 sm:px-6 lg:px-8'>
|
||||
<div className='sm:mx-auto sm:w-full sm:max-w-md'>
|
||||
<div className='bg-white py-8 px-4 shadow-xl ring-1 ring-gray-900/10 sm:rounded-lg sm:px-10'>
|
||||
<h1>
|
||||
{hasPaid === 'paid'
|
||||
{paymentStatus === 'paid'
|
||||
? '🥳 Payment Successful!'
|
||||
: hasPaid === 'canceled'
|
||||
: paymentStatus === 'canceled'
|
||||
? '😢 Payment Canceled'
|
||||
: hasPaid === 'error' && '🙄 Payment Error'}
|
||||
: paymentStatus === 'error' && '🙄 Payment Error'}
|
||||
</h1>
|
||||
{hasPaid !== 'loading' && (
|
||||
{paymentStatus !== 'loading' && (
|
||||
<span className='text-center'>
|
||||
You are being redirected to your account page... <br />
|
||||
</span>
|
||||
|
@ -28,7 +28,7 @@ export default function PricingPage() {
|
||||
const clickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await stripePayment();
|
||||
const response = await stripePayment('hobby');
|
||||
if (response?.sessionUrl) {
|
||||
window.open(response.sessionUrl, '_self');
|
||||
}
|
||||
|
6
src/client/const.ts
Normal file
6
src/client/const.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// get your own link from your stripe dashboard: https://dashboard.stripe.com/settings/billing/portal
|
||||
// and your own test link: https://dashboard.stripe.com/test/settings/billing/portal
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
export const CUSTOMER_PORTAL_LINK = isDev
|
||||
? 'https://billing.stripe.com/p/login/test_8wM8x17JN7DT4zC000'
|
||||
: '<insert-prod-link-here>';
|
@ -22,10 +22,10 @@ export function useReferrer() {
|
||||
console.log('referrer', referrer);
|
||||
if (!!refValue && refValue !== UNKOWN_REFERRER) {
|
||||
setReferrer(values);
|
||||
history.replace({
|
||||
search: '',
|
||||
});
|
||||
}
|
||||
history.replace({
|
||||
search: '',
|
||||
});
|
||||
}, [referrer]);
|
||||
|
||||
return [referrer, setReferrer] as const;
|
||||
|
@ -9,12 +9,33 @@ import daBoi from '../static/magic-app-gen-logo.png';
|
||||
import { features, navigation, tiers, faqs, footerNavigation } from './contentSections';
|
||||
import useAuth from '@wasp/auth/useAuth';
|
||||
import DropdownUser from '../components/DropdownUser';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import stripePayment from '@wasp/actions/stripePayment';
|
||||
import { CUSTOMER_PORTAL_LINK } from '../const';
|
||||
|
||||
export default function LandingPage() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const { data: user, isLoading: isUserLoading } = useAuth();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
async function handleBuyNowClick(tierId: string) {
|
||||
if (!user) {
|
||||
history.push('/login');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let stripeResults = await stripePayment(tierId);
|
||||
|
||||
if (stripeResults?.sessionUrl) {
|
||||
window.open(stripeResults.sessionUrl, '_self');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error?.message ?? 'Something went wrong.');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-white'>
|
||||
{/* Header */}
|
||||
@ -335,10 +356,12 @@ export default function LandingPage() {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<a
|
||||
aria-describedby={tier.id}
|
||||
href={!!user ? '/account' : '/login'}
|
||||
className={`
|
||||
{!!user && user.hasPaid ? (
|
||||
<a
|
||||
href={CUSTOMER_PORTAL_LINK}
|
||||
aria-describedby='manage-subscription'
|
||||
className={`
|
||||
${tier.id === 'enterprise-tier' ? 'opacity-50 cursor-not-allowed' : 'opacity-100 cursor-pointer'}
|
||||
${
|
||||
tier.bestDeal
|
||||
? 'bg-yellow-500 text-white hover:text-white shadow-sm hover:bg-yellow-400'
|
||||
@ -346,9 +369,26 @@ export default function LandingPage() {
|
||||
}
|
||||
'mt-8 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-yellow-400'
|
||||
`}
|
||||
>
|
||||
{!!user ? 'Buy plan' : 'Log in to buy plan'}
|
||||
</a>
|
||||
>
|
||||
{tier.id === 'enterprise-tier' ? 'Contact us' : 'Manage Subscription'}
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleBuyNowClick(tier.id)}
|
||||
aria-describedby={tier.id}
|
||||
className={`
|
||||
${tier.id === 'enterprise-tier' ? 'opacity-50 cursor-not-allowed' : 'opacity-100 cursor-pointer'}
|
||||
${
|
||||
tier.bestDeal
|
||||
? 'bg-yellow-500 text-white hover:text-white shadow-sm hover:bg-yellow-400'
|
||||
: 'text-gray-600 ring-1 ring-inset ring-purple-200 hover:ring-purple-400'
|
||||
}
|
||||
'mt-8 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-yellow-400'
|
||||
`}
|
||||
>
|
||||
{tier.id === 'enterprise-tier' ? 'Contact us' : !!user ? 'Buy plan' : 'Log in to buy plan'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -44,14 +44,14 @@ export const tiers = [
|
||||
{
|
||||
name: 'Hobby',
|
||||
id: 'hobby-tier',
|
||||
priceMonthly: 'free',
|
||||
description: 'try it out for free',
|
||||
features: ['5 credits', 'no expiration date', 'no credit card required'],
|
||||
priceMonthly: '$9.99',
|
||||
description: 'All you need to get started',
|
||||
features: ['Limited monthly usage', 'Basic support'],
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
id: 'pro-tier',
|
||||
priceMonthly: '$10',
|
||||
priceMonthly: '$19.99',
|
||||
description: 'Our most popular plan',
|
||||
features: ['Unlimited monthly usage', 'Priority customer support'],
|
||||
bestDeal: true,
|
||||
@ -59,7 +59,7 @@ export const tiers = [
|
||||
{
|
||||
name: 'Enterprise',
|
||||
id: 'enterprise-tier',
|
||||
priceMonthly: '$50',
|
||||
priceMonthly: '$500',
|
||||
description: 'Big business means big money',
|
||||
features: ['Unlimited monthly usage', '24/7 customer support', 'Advanced analytics'],
|
||||
},
|
||||
|
@ -4,70 +4,42 @@ import type { RelatedObject, User } from '@wasp/entities';
|
||||
import type { GenerateGptResponse, StripePayment } from '@wasp/actions/types';
|
||||
import type { StripePaymentResult, OpenAIResponse } from './types';
|
||||
import { UpdateCurrentUser, SaveReferrer, UpdateUserReferrer, UpdateUserById } from '@wasp/actions/types';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import { fetchStripeCustomer, createStripeCheckoutSession } from './stripeUtils.js';
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_KEY!, {
|
||||
apiVersion: '2022-11-15',
|
||||
});
|
||||
|
||||
// WASP_WEB_CLIENT_URL will be set up by Wasp when deploying to production: https://wasp-lang.dev/docs/deploying
|
||||
const DOMAIN = process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000';
|
||||
|
||||
export const stripePayment: StripePayment<void, StripePaymentResult> = async (_args, context) => {
|
||||
if (!context.user) {
|
||||
export const stripePayment: StripePayment<string, StripePaymentResult> = async (tier, context) => {
|
||||
if (!context.user || !context.user.email) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
let customer: Stripe.Customer;
|
||||
const stripeCustomers = await stripe.customers.list({
|
||||
email: context.user.email!,
|
||||
});
|
||||
if (!stripeCustomers.data.length) {
|
||||
console.log('creating customer');
|
||||
customer = await stripe.customers.create({
|
||||
email: context.user.email!,
|
||||
});
|
||||
} else {
|
||||
console.log('using existing customer');
|
||||
customer = stripeCustomers.data[0];
|
||||
}
|
||||
|
||||
const session: Stripe.Checkout.Session = await stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
price: process.env.SUBSCRIPTION_PRICE_ID!,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: 'subscription',
|
||||
success_url: `${DOMAIN}/checkout?success=true`,
|
||||
cancel_url: `${DOMAIN}/checkout?canceled=true`,
|
||||
automatic_tax: { enabled: true },
|
||||
customer_update: {
|
||||
address: 'auto',
|
||||
},
|
||||
customer: customer.id,
|
||||
});
|
||||
const priceId = tier === 'hobby-tier' ? 'HOBBY_SUBSCRIPTION_PRICE_ID' : 'PRO_SUBSCRIPTION_PRICE_ID';
|
||||
|
||||
let customer: Stripe.Customer;
|
||||
let session: Stripe.Checkout.Session;
|
||||
try {
|
||||
customer = await fetchStripeCustomer(context.user.email);
|
||||
session = await createStripeCheckoutSession({
|
||||
priceId,
|
||||
customerId: customer.id,
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new HttpError(500, error.message);
|
||||
}
|
||||
|
||||
await context.entities.User.update({
|
||||
where: {
|
||||
id: context.user.id,
|
||||
},
|
||||
data: {
|
||||
checkoutSessionId: session?.id ?? null,
|
||||
stripeId: customer.id ?? null,
|
||||
checkoutSessionId: session.id,
|
||||
stripeId: customer.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new HttpError(402, 'Could not create a Stripe session');
|
||||
} else {
|
||||
return {
|
||||
sessionUrl: session.url,
|
||||
sessionId: session.id,
|
||||
};
|
||||
}
|
||||
return {
|
||||
sessionUrl: session.url,
|
||||
sessionId: session.id,
|
||||
};
|
||||
};
|
||||
|
||||
type GptPayload = {
|
||||
@ -168,10 +140,10 @@ export const updateUserById: UpdateUserById<{ id: number; data: Partial<User> },
|
||||
data,
|
||||
});
|
||||
|
||||
console.log('updated user', updatedUser.id)
|
||||
console.log('updated user', updatedUser.id);
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCurrentUser: UpdateCurrentUser<Partial<User>, User> = async (user, context) => {
|
||||
if (!context.user) {
|
||||
@ -188,7 +160,6 @@ export const updateCurrentUser: UpdateCurrentUser<Partial<User>, User> = async (
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const saveReferrer: SaveReferrer<{ name: string }, void> = async ({ name }, context) => {
|
||||
await context.entities.Referrer.upsert({
|
||||
where: {
|
||||
@ -204,7 +175,7 @@ export const saveReferrer: SaveReferrer<{ name: string }, void> = async ({ name
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserReferrer: UpdateUserReferrer<{ name: string }, void> = async ({ name }, context) => {
|
||||
if (!context.user) {
|
||||
@ -222,4 +193,4 @@ export const updateUserReferrer: UpdateUserReferrer<{ name: string }, void> = as
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
44
src/server/stripeUtils.ts
Normal file
44
src/server/stripeUtils.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_KEY!, {
|
||||
apiVersion: '2022-11-15',
|
||||
});
|
||||
|
||||
// WASP_WEB_CLIENT_URL will be set up by Wasp when deploying to production: https://wasp-lang.dev/docs/deploying
|
||||
const DOMAIN = process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000';
|
||||
|
||||
export async function fetchStripeCustomer(customerEmail: string) {
|
||||
let customer: Stripe.Customer;
|
||||
const stripeCustomers = await stripe.customers.list({
|
||||
email: customerEmail,
|
||||
});
|
||||
if (!stripeCustomers.data.length) {
|
||||
console.log('creating customer');
|
||||
customer = await stripe.customers.create({
|
||||
email: customerEmail,
|
||||
});
|
||||
} else {
|
||||
console.log('using existing customer');
|
||||
customer = stripeCustomers.data[0];
|
||||
}
|
||||
return customer;
|
||||
}
|
||||
|
||||
export async function createStripeCheckoutSession({ priceId, customerId }: { priceId: string; customerId: string }) {
|
||||
return await stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
price: process.env[priceId]!,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: 'subscription',
|
||||
success_url: `${DOMAIN}/checkout?success=true`,
|
||||
cancel_url: `${DOMAIN}/checkout?canceled=true`,
|
||||
automatic_tax: { enabled: true },
|
||||
customer_update: {
|
||||
address: 'auto',
|
||||
},
|
||||
customer: customerId,
|
||||
});
|
||||
}
|
@ -1,23 +1,9 @@
|
||||
import express from 'express';
|
||||
import { StripeWebhook } from '@wasp/apis/types';
|
||||
import type { MiddlewareConfigFn } from '@wasp/middleware';
|
||||
import { emailSender } from '@wasp/email/index.js';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import requestIp from 'request-ip';
|
||||
|
||||
export const STRIPE_WEBHOOK_IPS = [
|
||||
'3.18.12.63',
|
||||
'3.130.192.231',
|
||||
'13.235.14.237',
|
||||
'13.235.122.149',
|
||||
'18.211.135.69',
|
||||
'35.154.171.200',
|
||||
'52.15.183.38',
|
||||
'54.88.130.119',
|
||||
'54.88.130.237',
|
||||
'54.187.174.169',
|
||||
'54.187.205.235',
|
||||
'54.187.216.72',
|
||||
];
|
||||
|
||||
// make sure the api version matches the version in the Stripe dashboard
|
||||
const stripe = new Stripe(process.env.STRIPE_KEY!, {
|
||||
@ -25,22 +11,23 @@ const stripe = new Stripe(process.env.STRIPE_KEY!, {
|
||||
});
|
||||
|
||||
export const stripeWebhook: StripeWebhook = async (request, response, context) => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const detectedIp = requestIp.getClientIp(request) as string;
|
||||
const isStripeIP = STRIPE_WEBHOOK_IPS.includes(detectedIp);
|
||||
const sig = request.headers['stripe-signature'] as string;
|
||||
let event: Stripe.Event;
|
||||
|
||||
if (!isStripeIP) {
|
||||
console.log('IP address not from Stripe: ', detectedIp);
|
||||
return response.status(403).json({ received: false });
|
||||
}
|
||||
console.log('\n\nsig: ', sig)
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(request.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
|
||||
// console.table({sig: 'stripe webhook signature verified', type: event.type})
|
||||
} catch (err: any) {
|
||||
console.log(err.message);
|
||||
return response.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
|
||||
let event: Stripe.Event;
|
||||
// let event: Stripe.Event;
|
||||
let userStripeId: string | null = null;
|
||||
|
||||
try {
|
||||
event = request.body;
|
||||
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
console.log('Checkout session completed');
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
@ -52,8 +39,8 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
|
||||
console.log('line_items: ', line_items);
|
||||
|
||||
if (line_items?.data[0]?.price?.id === process.env.SUBSCRIPTION_PRICE_ID) {
|
||||
console.log('Subscription purchased ');
|
||||
if (line_items?.data[0]?.price?.id === process.env.HOBBY_SUBSCRIPTION_PRICE_ID) {
|
||||
console.log('Hobby subscription purchased ');
|
||||
await context.entities.User.updateMany({
|
||||
where: {
|
||||
stripeId: userStripeId,
|
||||
@ -61,6 +48,19 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
data: {
|
||||
hasPaid: true,
|
||||
datePaid: new Date(),
|
||||
subscriptionType: 'hobby',
|
||||
},
|
||||
});
|
||||
} else if (line_items?.data[0]?.price?.id === process.env.PRO_SUBSCRIPTION_PRICE_ID) {
|
||||
console.log('Pro subscription purchased ');
|
||||
await context.entities.User.updateMany({
|
||||
where: {
|
||||
stripeId: userStripeId,
|
||||
},
|
||||
data: {
|
||||
hasPaid: true,
|
||||
datePaid: new Date(),
|
||||
subscriptionType: 'pro',
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -190,3 +190,27 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
response.status(400).send(`Webhook Error: ${err?.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// MIDDELWARE EXAMPLE
|
||||
// const defaultGlobalMiddleware: MiddlewareConfig = new Map([
|
||||
// ['helmet', helmet()],
|
||||
// ['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
// ['logger', logger('dev')],
|
||||
// ['express.json', express.json()],
|
||||
// ['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
// ['cookieParser', cookieParser()],
|
||||
// ]);
|
||||
|
||||
export const stripeMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
middlewareConfig.delete('express.json');
|
||||
middlewareConfig.set('express.raw', express.raw({ type: 'application/json' }));
|
||||
return middlewareConfig;
|
||||
|
||||
// let updatedMiddlewareConfig = new Map([
|
||||
// // New entry as an array: [key, value]
|
||||
// ['express.raw', express.raw({ type: 'application/json' })],
|
||||
// ...Array.from(middlewareConfig.entries()),
|
||||
// ]);
|
||||
|
||||
// return updatedMiddlewareConfig;
|
||||
};
|
@ -64,12 +64,6 @@ export const calculateDailyStats: DailyStats<never, void> = async (_args, contex
|
||||
},
|
||||
});
|
||||
|
||||
await context.entities.Logs.create({
|
||||
data: {
|
||||
message: `Daily stats calculated for ${nowUTC.toDateString()}`,
|
||||
level: 'job-info',
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error calculating daily stats: ', error);
|
||||
await context.entities.Logs.create({
|
||||
|
Loading…
x
Reference in New Issue
Block a user