update webhook and doc links

This commit is contained in:
vincanger 2023-10-12 17:54:15 +02:00
parent 90566cf5d3
commit aade090e33
6 changed files with 66 additions and 17 deletions

View File

@ -1,6 +1,7 @@
# NOTE: if you setup your DB using `wasp start db` then you DO NOT need to add a DATABASE_URL env. # NOTE: if you setup your DB using `wasp start db` then you DO NOT need to add a DATABASE_URL env.
# DATABASE_URL= # DATABASE_URL=
# for testing, go to https://dashboard.stripe.com/test/apikeys and get a test stripe key that starts with "sk_test_..."
STRIPE_KEY= STRIPE_KEY=
SUBSCRIPTION_PRICE_ID= SUBSCRIPTION_PRICE_ID=

View File

@ -1,6 +1,6 @@
app SaaSTemplate { app SaaSTemplate {
wasp: { wasp: {
version: "^0.11.1" version: "^0.11.6"
}, },
title: "My SaaS App", title: "My SaaS App",
head: [ head: [
@ -10,7 +10,7 @@ app SaaSTemplate {
"<meta property='og:image' content='src/client/static/image.png' />", "<meta property='og:image' content='src/client/static/image.png' />",
// you can put your google analytics script here, too! // you can put your google analytics script here, too!
], ],
// 🔐 Auth out of the box! https://wasp-lang.dev/docs/language/features#authentication--authorization // 🔐 Auth out of the box! https://wasp-lang.dev/docs/auth/overview
auth: { auth: {
userEntity: User, userEntity: User,
externalAuthEntity: SocialLogin, externalAuthEntity: SocialLogin,
@ -30,7 +30,7 @@ app SaaSTemplate {
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js", getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
}, },
}, },
google: { // Guide for setting up Auth via Google https://wasp-lang.dev/docs/integrations/google google: { // Guide for setting up Auth via Google https://wasp-lang.dev/docs/auth/social-auth/overview
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js", getUserFieldsFn: import { getUserFields } from "@server/auth/google.js",
configFn: import { config } from "@server/auth/google.js", configFn: import { config } from "@server/auth/google.js",
}, },
@ -60,13 +60,13 @@ app SaaSTemplate {
("request-ip", "3.3.0"), ("request-ip", "3.3.0"),
("@types/request-ip", "0.0.37"), ("@types/request-ip", "0.0.37"),
("node-fetch", "3.3.0"), ("node-fetch", "3.3.0"),
("react-hook-form", "7.43.1"), ("react-hook-form", "^7.45.4"),
("stripe", "11.15.0"), ("stripe", "11.15.0"),
], ],
} }
/* 💽 Wasp defines DB entities via Prisma Database Models: /* 💽 Wasp defines DB entities via Prisma Database Models:
* https://wasp-lang.dev/docs/language/features#entity * https://wasp-lang.dev/docs/data-model/entities
*/ */
entity User {=psl entity User {=psl
@ -80,6 +80,7 @@ entity User {=psl
checkoutSessionId String? checkoutSessionId String?
hasPaid Boolean @default(false) hasPaid Boolean @default(false)
sendEmail Boolean @default(false) sendEmail Boolean @default(false)
subscriptionStatus String?
datePaid DateTime? datePaid DateTime?
credits Int @default(3) credits Int @default(3)
relatedObject RelatedObject[] relatedObject RelatedObject[]
@ -108,7 +109,7 @@ psl=}
/* 📡 These are the Wasp Routes (You can protect them easily w/ 'authRequired: true'); /* 📡 These are the Wasp Routes (You can protect them easily w/ 'authRequired: true');
* https://wasp-lang.dev/docs/language/features#route * https://wasp-lang.dev/docs/tutorial/pages
*/ */
route RootRoute { path: "/", to: MainPage } route RootRoute { path: "/", to: MainPage }
@ -164,7 +165,7 @@ page CheckoutPage {
} }
/* ⛑ These are the Wasp Operations, which allow the client and server to interact: /* ⛑ These are the Wasp Operations, which allow the client and server to interact:
* https://wasp-lang.dev/docs/language/features#queries-and-actions-aka-operations * https://wasp-lang.dev/docs/data-model/operations/overview
*/ */
// 📝 Actions aka Mutations // 📝 Actions aka Mutations
@ -198,7 +199,7 @@ query getRelatedObjects {
/* /*
* 📡 These are custom Wasp API Endpoints. Use them for callbacks, webhooks, etc. * 📡 These are custom Wasp API Endpoints. Use them for callbacks, webhooks, etc.
* https://wasp-lang.dev/docs/language/features#apis * https://wasp-lang.dev/docs/advanced/apis
*/ */
api stripeWebhook { api stripeWebhook {
@ -208,7 +209,7 @@ api stripeWebhook {
} }
/* 🕵️‍♂️ These are the Wasp Cron Jobs. Use them to set up recurring tasks and/or queues: /* 🕵️‍♂️ These are the Wasp Cron Jobs. Use them to set up recurring tasks and/or queues:
* https://wasp-lang.dev/docs/language/features#jobs * https://wasp-lang.dev/docs/advanced/jobs
*/ */
job emailChecker { job emailChecker {

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "subscriptionStatus" TEXT;

View File

@ -70,12 +70,18 @@ export default function Example({ user }: { user: User }) {
function BuyMoreButton({ isLoading, setIsLoading }: { isLoading: boolean, setIsLoading: Dispatch<SetStateAction<boolean>> }) { function BuyMoreButton({ isLoading, setIsLoading }: { isLoading: boolean, setIsLoading: Dispatch<SetStateAction<boolean>> }) {
const handleClick = async () => { const handleClick = async () => {
setIsLoading(true); try {
const stripeResults = await stripePayment(); setIsLoading(true);
if (stripeResults?.sessionUrl) { const stripeResults = await stripePayment();
window.open(stripeResults.sessionUrl, '_self'); if (stripeResults?.sessionUrl) {
window.open(stripeResults.sessionUrl, '_self');
}
} catch (error: any) {
alert(error?.message ?? 'Something went wrong.')
} finally {
setIsLoading(false);
} }
setIsLoading(false);
}; };
return ( return (

View File

@ -16,6 +16,7 @@ export const stripePayment: StripePayment<void, StripePaymentResult> = async (_a
if (!context.user) { if (!context.user) {
throw new HttpError(401); throw new HttpError(401);
} }
let customer: Stripe.Customer; let customer: Stripe.Customer;
const stripeCustomers = await stripe.customers.list({ const stripeCustomers = await stripe.customers.list({
email: context.user.email!, email: context.user.email!,

View File

@ -24,7 +24,6 @@ const stripe = new Stripe(process.env.STRIPE_KEY!, {
}); });
export const stripeWebhook: StripeWebhook = async (request, response, context) => { export const stripeWebhook: StripeWebhook = async (request, response, context) => {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
const detectedIp = requestIp.getClientIp(request) as string; const detectedIp = requestIp.getClientIp(request) as string;
const isStripeIP = STRIPE_WEBHOOK_IPS.includes(detectedIp); const isStripeIP = STRIPE_WEBHOOK_IPS.includes(detectedIp);
@ -42,6 +41,7 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
event = request.body; event = request.body;
if (event.type === 'checkout.session.completed') { if (event.type === 'checkout.session.completed') {
console.log('Checkout session completed');
const session = event.data.object as Stripe.Checkout.Session; const session = event.data.object as Stripe.Checkout.Session;
userStripeId = session.customer as string; userStripeId = session.customer as string;
@ -59,6 +59,7 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
}, },
data: { data: {
hasPaid: true, hasPaid: true,
datePaid: new Date(),
}, },
}); });
} }
@ -81,10 +82,47 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
// }, // },
// }); // });
// } // }
} else if (event.type === 'invoice.paid') {
console.log('>>>> invoice.paid: ', userStripeId);
const invoice = event.data.object as Stripe.Invoice;
const periodStart = new Date(invoice.period_start * 1000);
await context.entities.User.updateMany({
where: {
stripeId: userStripeId,
},
data: {
hasPaid: true,
datePaid: periodStart,
},
});
} else if (event.type === 'customer.subscription.updated') { } else if (event.type === 'customer.subscription.updated') {
const subscription = event.data.object as Stripe.Subscription; const subscription = event.data.object as Stripe.Subscription;
userStripeId = subscription.customer as string; userStripeId = subscription.customer as string;
if (subscription.status === 'active') {
console.log('Subscription active ', userStripeId);
await context.entities.User.updateMany({
where: {
stripeId: userStripeId,
},
data: {
subscriptionStatus: 'active',
},
});
}
// you'll want to make a check on the front end to see if the subscription is past due
// and then prompt the user to update their payment method
// this is useful if the user's card expires or is canceled and automatic subscription renewal fails
if (subscription.status === 'past_due') {
console.log('Subscription past due: ', userStripeId);
await context.entities.User.updateMany({
where: {
stripeId: userStripeId,
},
data: {
subscriptionStatus: 'past_due',
},
});
}
/** /**
* Stripe will send a subscription.updated event when a subscription is canceled * Stripe will send a subscription.updated event when a subscription is canceled
* but the subscription is still active until the end of the period. * but the subscription is still active until the end of the period.