diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 323abe1..ef192e5 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -11,6 +11,8 @@ env: DATABASE_URL: ${{ secrets.DATABASE_URL }} JWT_SECRET: ${{ secrets.JWT_SECRET }} ADS_SERVER_URL: ${{ vars.ADS_SERVER_URL }} + STRIPE_KEY: ${{ secrets.STRIPE_KEY }} + SUBSCRIPTION_PRICE_ID: ${{ secrets.SUBSCRIPTION_PRICE_ID }} jobs: unit_test: diff --git a/src/client/AccountPage.tsx b/src/client/AccountPage.tsx index 4277c33..31ffe87 100644 --- a/src/client/AccountPage.tsx +++ b/src/client/AccountPage.tsx @@ -4,10 +4,10 @@ import { useQuery } from "@wasp/queries"; import logout from "@wasp/auth/logout"; import stripePayment from "@wasp/actions/stripePayment"; import { useState, Dispatch, SetStateAction } from "react"; +import { Link } from "@wasp/router"; // 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"; +const CUSTOMER_PORTAL_LINK = ""; export default function Example({ user }: { user: User }) { const [isLoading, setIsLoading] = useState(false); @@ -32,34 +32,81 @@ export default function Example({ user }: { user: User }) { {user.email} - {/*
-
Your Plan
+
+ {/*
+ Credits remaining +
+ <> +
+ {user.credits} +
+ + */} + {/*
Your Plan
{user.hasPaid ? ( <> -
Premium Monthly Subscription
- +
+ Premium Subscription +
+ ) : ( <> -
+
Credits remaining: {user.credits}
- + )} -
*/} - {/*
-
About
-
I'm a cool customer.
-
*/} - {/*
*/} - {/*
Most Recent User RelatedObject
*/} - {/*
+ */} +
Your Plan
+ {user.hasPaid ? ( + <> +
+ Premium Subscription +
+ + + ) : ( + <> +
+ No Active Subscription +
+ + + )} +
+
+
About
+
+ I'm a cool customer. +
+
+ {/*
+
+ Most Recent User RelatedObject +
+
{!!relatedObjects && relatedObjects.length > 0 ? relatedObjects[relatedObjects.length - 1].content : "You don't have any at this time."} -
*/} - {/*
*/} + +
*/} @@ -98,14 +145,17 @@ function BuyMoreButton({ return (
- + {!isLoading ? Upgrade Plan : "Loading..."} + */}
); } diff --git a/src/client/NavBar.tsx b/src/client/NavBar.tsx index 042c980..4a84a81 100644 --- a/src/client/NavBar.tsx +++ b/src/client/NavBar.tsx @@ -1,83 +1,108 @@ +import logo from "./static/captn-logo.png"; +import { Disclosure } from "@headlessui/react"; +import { AiOutlineBars, AiOutlineClose, AiOutlineUser } from "react-icons/ai"; +import useAuth from "@wasp/auth/useAuth"; -import logo from './static/captn-logo.png' -import { Disclosure } from '@headlessui/react'; -import { AiOutlineBars, AiOutlineClose, AiOutlineUser } from 'react-icons/ai'; -import useAuth from '@wasp/auth/useAuth'; - -const active = 'inline-flex items-center border-b-2 border-indigo-300 px-1 pt-1 text-sm font-medium text-gray-900'; -const inactive = 'inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700' +const active = + "inline-flex items-center border-b-2 border-indigo-300 px-1 pt-1 text-sm font-medium text-gray-900"; +const inactive = + "inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"; const current = window.location.pathname; export default function NavBar() { const { data: user } = useAuth(); return ( - + {({ open }) => ( <> -
-
-
-
- - My SaaS App +
+
+
+ - -
- - + -
+
{/* Mobile menu */} - - Open menu + + Open menu {open ? ( -
- -
+ +
Home Chat Account diff --git a/src/client/PricingPage.tsx b/src/client/PricingPage.tsx index 5a93c19..864181f 100644 --- a/src/client/PricingPage.tsx +++ b/src/client/PricingPage.tsx @@ -1,23 +1,25 @@ import { AiOutlineCheck } from "react-icons/ai"; import stripePayment from "@wasp/actions/stripePayment"; import { useState } from "react"; +import NumericStepper from "./components/NumericStepper"; const prices = [ + // { + // name: "Credits", + // id: "credits", + // href: "", + // price: "$", + // description: "Buy credits to use for your projects.", + // features: ["Use them any time", "No expiration date"], + // disabled: false, + // priceMonthly: "", + // }, { - name: "Credits", - id: "credits", - href: "", - price: "$2.95", - description: "Buy credits to use for your projects.", - features: ["10 credits", "Use them any time", "No expiration date"], - disabled: true, - }, - { - name: "Monthly Subscription", - id: "monthly", + name: "Lifetime Subscription", + id: "Lifetime", href: "#", - priceMonthly: "$9.99", - description: "Get unlimited usage for your projects.", + priceMonthly: "$1", + description: "Get access to all premium features.", features: [ "Unlimited usage of all features", "Priority support", @@ -47,11 +49,11 @@ export default function PricingPage() { return (
-
+
{prices.map((price) => (

{price.priceMonthly || price.price} - {price.priceMonthly && ( + {/* {price.priceMonthly && ( /month - )} + )} */}

{price.description} diff --git a/src/client/components/NumericStepper.tsx b/src/client/components/NumericStepper.tsx new file mode 100644 index 0000000..faf19d8 --- /dev/null +++ b/src/client/components/NumericStepper.tsx @@ -0,0 +1,53 @@ +import React, { useState } from "react"; + +const NumericStepper = ({ + value, + minValue, + maxValue, + step, + onChange, +}: { + value: any; + minValue: any; + maxValue: any; + step: any; + onChange: any; +}) => { + const [currentValue, setCurrentValue] = useState(value); + + const increment = () => { + if (currentValue + step <= maxValue) { + setCurrentValue(currentValue + step); + onChange(currentValue + step); + } + }; + + const decrement = () => { + if (currentValue - step >= minValue) { + setCurrentValue(currentValue - step); + onChange(currentValue - step); + } + }; + + return ( +

+ + {/* {currentValue} */} + +
+ ); +}; + +export default NumericStepper; diff --git a/src/server/actions.ts b/src/server/actions.ts index f0d84b5..894cd63 100644 --- a/src/server/actions.ts +++ b/src/server/actions.ts @@ -49,7 +49,7 @@ export const stripePayment: StripePayment = async ( quantity: 1, }, ], - mode: "subscription", + mode: "payment", success_url: `${DOMAIN}/checkout?success=true`, cancel_url: `${DOMAIN}/checkout?canceled=true`, automatic_tax: { enabled: true }, diff --git a/src/server/webhooks.ts b/src/server/webhooks.ts index 4d49656..947bc16 100644 --- a/src/server/webhooks.ts +++ b/src/server/webhooks.ts @@ -1,35 +1,39 @@ -import { StripeWebhook } from '@wasp/apis/types'; -import { emailSender } from '@wasp/email/index.js'; +import { StripeWebhook } from "@wasp/apis/types"; +import { emailSender } from "@wasp/email/index.js"; -import Stripe from 'stripe'; -import requestIp from 'request-ip'; +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', + "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", ]; const stripe = new Stripe(process.env.STRIPE_KEY!, { - apiVersion: '2022-11-15', + apiVersion: "2022-11-15", }); -export const stripeWebhook: StripeWebhook = async (request, response, context) => { - if (process.env.NODE_ENV === 'production') { +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); if (!isStripeIP) { - console.log('IP address not from Stripe: ', detectedIp); + console.log("IP address not from Stripe: ", detectedIp); return response.status(403).json({ received: false }); } } @@ -40,19 +44,24 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) = try { event = request.body; - if (event.type === 'checkout.session.completed') { - console.log('Checkout session completed'); + if (event.type === "checkout.session.completed") { + console.log("Checkout session completed"); const session = event.data.object as Stripe.Checkout.Session; userStripeId = session.customer as string; - const { line_items } = await stripe.checkout.sessions.retrieve(session.id, { - expand: ['line_items'], - }); + const { line_items } = await stripe.checkout.sessions.retrieve( + session.id, + { + expand: ["line_items"], + } + ); - console.log('line_items: ', line_items); + 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.SUBSCRIPTION_PRICE_ID + ) { + console.log("Subscription purchased "); await context.entities.User.updateMany({ where: { stripeId: userStripeId, @@ -68,22 +77,21 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) = * and here is an example of handling a different type of product * make sure to configure it in the Stripe dashboard first! */ - - // if (line_items?.data[0]?.price?.id === process.env.CREDITS_PRICE_ID) { - // console.log('Credits purchased: '); - // await context.entities.User.updateMany({ - // where: { - // stripeId: userStripeId, - // }, - // data: { - // credits: { - // increment: 10, - // }, - // }, - // }); - // } - } else if (event.type === 'invoice.paid') { - console.log('>>>> invoice.paid: ', userStripeId); + if (line_items?.data[0]?.price?.id === process.env.CREDITS_PRICE_ID) { + console.log("Credits purchased: "); + await context.entities.User.updateMany({ + where: { + stripeId: userStripeId, + }, + data: { + credits: { + increment: 10, + }, + }, + }); + } + } 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({ @@ -95,31 +103,31 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) = datePaid: periodStart, }, }); - } else if (event.type === 'customer.subscription.updated') { + } else if (event.type === "customer.subscription.updated") { const subscription = event.data.object as Stripe.Subscription; userStripeId = subscription.customer as string; - if (subscription.status === 'active') { - console.log('Subscription active ', userStripeId); + if (subscription.status === "active") { + console.log("Subscription active ", userStripeId); await context.entities.User.updateMany({ where: { stripeId: userStripeId, }, data: { - subscriptionStatus: 'active', + 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); + if (subscription.status === "past_due") { + console.log("Subscription past due: ", userStripeId); await context.entities.User.updateMany({ where: { stripeId: userStripeId, }, data: { - subscriptionStatus: 'past_due', + subscriptionStatus: "past_due", }, }); } @@ -130,7 +138,7 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) = * https://stripe.com/docs/billing/subscriptions/cancel#events */ if (subscription.cancel_at_period_end) { - console.log('Subscription canceled at period end'); + console.log("Subscription canceled at period end"); const customer = await context.entities.User.findFirst({ where: { @@ -144,13 +152,16 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) = if (customer?.email) { await emailSender.send({ to: customer.email, - subject: 'We hate to see you go :(', - text: 'We hate to see you go. Here is a sweet offer...', - html: 'We hate to see you go. Here is a sweet offer...', + subject: "We hate to see you go :(", + text: "We hate to see you go. Here is a sweet offer...", + html: "We hate to see you go. Here is a sweet offer...", }); } } - } else if (event.type === 'customer.subscription.deleted' || event.type === 'customer.subscription.canceled') { + } else if ( + event.type === "customer.subscription.deleted" || + event.type === "customer.subscription.canceled" + ) { const subscription = event.data.object as Stripe.Subscription; userStripeId = subscription.customer as string; @@ -158,7 +169,7 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) = * Stripe will send then finally send a subscription.deleted event when subscription period ends * https://stripe.com/docs/billing/subscriptions/cancel#events */ - console.log('Subscription deleted/ended'); + console.log("Subscription deleted/ended"); await context.entities.User.updateMany({ where: { stripeId: userStripeId,