mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-06-25 16:23:24 +02:00
update webhook and doc links
This commit is contained in:
parent
90566cf5d3
commit
aade090e33
@ -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=
|
||||||
|
|
||||||
|
19
main.wasp
19
main.wasp
@ -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 {
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "subscriptionStatus" TEXT;
|
@ -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 (
|
||||||
|
@ -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!,
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user