diff --git a/Backend/bin/app-amd64-linux b/Backend/bin/app-amd64-linux index 027ab6b..a5bdf15 100755 Binary files a/Backend/bin/app-amd64-linux and b/Backend/bin/app-amd64-linux differ diff --git a/Backend/main.go b/Backend/main.go index 40cac67..d95b182 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -391,23 +391,28 @@ func main() { form = forms.NewRecordUpsert(app, record) } - form.LoadData(map[string]any{ - "price_id": price.ID, - "product_id": price.Product.ID, - "active": price.Active, - "currency": price.Currency, - "description": price.Nickname, - "type": price.Type, - "unit_amount": price.UnitAmount, - "interval": price.Recurring.Interval, - "interval_count": price.Recurring.IntervalCount, - "trial_period_days": price.Recurring.TrialPeriodDays, - "metadata": price.Metadata, - }) + data := map[string]any{ + "price_id": price.ID, + "product_id": price.Product.ID, + "active": price.Active, + "currency": price.Currency, + "description": price.Nickname, + "type": price.Type, + "unit_amount": price.UnitAmount, + "metadata": price.Metadata, + } + // Check if Recurring is not nil before accessing its fields + if price.Recurring != nil { + data["interval"] = price.Recurring.Interval + data["interval_count"] = price.Recurring.IntervalCount + data["trial_period_days"] = price.Recurring.TrialPeriodDays + } + + form.LoadData(data) // validate and submit (internally it calls app.Dao().SaveRecord(record) in a transaction) if err := form.Submit(); err != nil { - return err + return c.JSON(http.StatusBadRequest, map[string]string{"failure": "failed to submit to pocketbase"}) } case "customer.subscription.created", "customer.subscription.updated", "customer.subscription.deleted": var subscription stripe.Subscription diff --git a/Frontend/app/(admin)/pricing/actions.ts b/Frontend/app/(admin)/pricing/actions.ts index f810a2b..3fa2285 100644 --- a/Frontend/app/(admin)/pricing/actions.ts +++ b/Frontend/app/(admin)/pricing/actions.ts @@ -2,7 +2,6 @@ import { Product, Price } from "@/types"; import pb from "@/lib/pocketbase"; -import { getAuthCookie } from "@/app/(auth)/actions"; export async function apiPrices() { try { @@ -30,11 +29,12 @@ export async function apiPrices() { product.metadata.benefits = product?.metadata?.benefits ? JSON.parse(product.metadata.benefits as any) : []; const pricesOfProduct = prices.filter(price => price.product_id === product.product_id); for (const priceOfProduct of pricesOfProduct){ - if (priceOfProduct.interval === "year"){ - product.yearlyPrice = priceOfProduct; - } else { - product.monthlyPrice = priceOfProduct; - } + product.type = priceOfProduct.type; + if (priceOfProduct.interval === "year"){ + product.yearlyPrice = priceOfProduct; + } else { + product.monthlyPrice = priceOfProduct; + } } } diff --git a/Frontend/app/(admin)/pricing/page.tsx b/Frontend/app/(admin)/pricing/page.tsx index 1e0e012..2e75c72 100644 --- a/Frontend/app/(admin)/pricing/page.tsx +++ b/Frontend/app/(admin)/pricing/page.tsx @@ -1,35 +1,13 @@ "use client"; -import React, { useEffect } from "react"; +import React from "react"; import PageHeader from "@/sections/PageHeader"; -import { Price, Product, SourceModal } from "@/types"; -import { Check } from "@styled-icons/entypo/Check"; -import { ChangeEventHandler, useState } from "react"; -import { apiPrices } from "./actions"; import Newsletter from "@/sections/Newsletter/Newsletter"; -import { createCheckoutSession, isAuthenticated } from "@/app/(auth)/actions"; -import { toast } from "react-toastify"; -import { useRouter } from "next/navigation"; import Background from "@/components/Utilities/Background"; import Footer from "@/components/Footer"; +import Payment from "@/sections/Payment"; export default function PricingPage() { - const [isAnnual, setIsAnnual] = useState(false); - const [products, setProducts] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - const handleToggle = () => { - setIsAnnual((prev) => !prev); - }; - useEffect(() => { - setIsLoading(true); - (async () => { - const resposeProducts: Product[] = await apiPrices(); - setProducts(resposeProducts); - setIsLoading(false); - })(); - }, []); - return (
} /> -
- -
-
-
- {isLoading ? ( - <> - - - - - ) : ( - products.map((x, i) => ( - - )) - )} -
-
+
); } - -function PriceCard({ - product, - isAnnual, - loading, -}: { - product?: Product; - isAnnual?: boolean; - loading?: boolean; -}) { - const router = useRouter(); - const openSignUpModalOnPriceClick = (price: Price) => { - const signUpModal = document.getElementById("sign-up-modal"); - if (!signUpModal) return; - signUpModal.setAttribute("price_id", price.price_id); - signUpModal.setAttribute("name", SourceModal.SignUpViaPurchase); - signUpModal.click(); - }; - const generateCheckoutPage = async (price: Price) => { - try { - const checkoutSessionResponse = await createCheckoutSession( - price.price_id - ); - router.push(checkoutSessionResponse.url); - } catch (error) { - if (error instanceof Error) { - toast.error(error.message, { - position: "bottom-left", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "colored", - }); - } - } - }; - const submitForm = async (price: Price) => { - const userIsAuthenticated = await isAuthenticated(); - if (userIsAuthenticated) { - await generateCheckoutPage(price); - } else { - localStorage.setItem("price", JSON.stringify(price)); - openSignUpModalOnPriceClick(price); - } - }; - return ( -
-
-
- {false && ( -

- Popular -

- )} - -

- {product?.name} -

-

- {product?.description} -

-
-
-
    - {product?.metadata?.benefits?.map((x, i) => ( -
  • - - -

    - {x} -

    -
  • - ))} -
-
-
- {!loading && ( -
-

- $ - {isAnnual - ? (product?.yearlyPrice?.unit_amount ?? 0) / 100 - : (product?.monthlyPrice?.unit_amount ?? 0) / 100} -

-

- per user per {isAnnual ? "year" : "month"} -

-
- )} - {product && ( - - )} -
-
-
- ); -} - -function PriceToggle({ - isAnnual, - onChange, -}: { - isAnnual: boolean; - onChange?: ChangeEventHandler; -}) { - return ( - <> - - - ); -} diff --git a/Frontend/app/(auth)/actions.ts b/Frontend/app/(auth)/actions.ts index 001b095..87378de 100644 --- a/Frontend/app/(auth)/actions.ts +++ b/Frontend/app/(auth)/actions.ts @@ -174,7 +174,7 @@ export async function logout() { redirect('/'); } -export async function createCheckoutSession(price_id: string) { +export async function createCheckoutSession(price_id: string, type: string) { const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL_STRING; if (!pocketbaseUrl) { throw Error('Connection Timeout'); @@ -200,7 +200,7 @@ export async function createCheckoutSession(price_id: string) { body: JSON.stringify({ price: { id: price_id, - type: "recurring" + type: type }, quantity: 1 }), diff --git a/Frontend/components/Modals/ModalSignUp.tsx b/Frontend/components/Modals/ModalSignUp.tsx index cec497e..a40b374 100644 --- a/Frontend/components/Modals/ModalSignUp.tsx +++ b/Frontend/components/Modals/ModalSignUp.tsx @@ -20,10 +20,11 @@ function ModalSignUp() { resolver: yupResolver(signUpValidationSchema), }); const router = useRouter(); - const generateCheckoutPage = async (price: Price) => { + const generateCheckoutPage = async (price: Price, type: string) => { try { const checkoutSessionResponse = await createCheckoutSession( - price.price_id + price.price_id, + type ); console.log(checkoutSessionResponse); router.push(checkoutSessionResponse.url); @@ -66,8 +67,11 @@ function ModalSignUp() { ) { reset(); document.getElementById("sign-up-modal")?.click(); + const type = document + .getElementById("sign-up-modal") + ?.getAttribute("type"); const price = localStorage.getItem("price"); - price && generateCheckoutPage(JSON.parse(price)); + price && generateCheckoutPage(JSON.parse(price), type ?? ""); } } catch (error) { console.log("heyaa"); diff --git a/Frontend/components/PriceCard.tsx b/Frontend/components/PriceCard.tsx new file mode 100644 index 0000000..94717c6 --- /dev/null +++ b/Frontend/components/PriceCard.tsx @@ -0,0 +1,117 @@ +import { Price, Product, SourceModal } from "@/types"; +import { Check } from "@styled-icons/entypo/Check"; +import { createCheckoutSession, isAuthenticated } from "@/app/(auth)/actions"; +import { toast } from "react-toastify"; +import { useRouter } from "next/navigation"; + +export default function PriceCard({ + product, + isAnnual, + loading, +}: { + product?: Product; + isAnnual?: boolean; + loading?: boolean; +}) { + const router = useRouter(); + const openSignUpModalOnPriceClick = (price: Price, type: string) => { + const signUpModal = document.getElementById("sign-up-modal"); + if (!signUpModal) return; + signUpModal.setAttribute("price_id", price.price_id); + signUpModal.setAttribute("type", type); + signUpModal.setAttribute("name", SourceModal.SignUpViaPurchase); + signUpModal.click(); + }; + const generateCheckoutPage = async (price: Price, type: string) => { + try { + const checkoutSessionResponse = await createCheckoutSession( + price.price_id, + type + ); + router.push(checkoutSessionResponse.url); + } catch (error) { + if (error instanceof Error) { + toast.error(error.message, { + position: "bottom-left", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "colored", + }); + } + } + }; + const submitForm = async (product: Product) => { + const userIsAuthenticated = await isAuthenticated(); + const price = isAnnual ? product.yearlyPrice : product.monthlyPrice; + if (userIsAuthenticated) { + await generateCheckoutPage(price, product.type); + } else { + localStorage.setItem("price", JSON.stringify(price)); + openSignUpModalOnPriceClick(price, product.type); + } + }; + return ( +
+
+
+ {false && ( +

+ Popular +

+ )} + +

+ {product?.name} +

+

+ {product?.description} +

+
+
+
    + {product?.metadata?.benefits?.map((x, i) => ( +
  • + + +

    + {x} +

    +
  • + ))} +
+
+
+ {!loading && ( +
+

+ $ + {isAnnual + ? (product?.yearlyPrice?.unit_amount ?? 0) / 100 + : (product?.monthlyPrice?.unit_amount ?? 0) / 100} +

+

+ per user per {isAnnual ? "year" : "month"} +

+
+ )} + {product && ( + + )} +
+
+
+ ); +} diff --git a/Frontend/components/PriceToggle.tsx b/Frontend/components/PriceToggle.tsx new file mode 100644 index 0000000..10ec11d --- /dev/null +++ b/Frontend/components/PriceToggle.tsx @@ -0,0 +1,36 @@ +import { ChangeEventHandler } from "react"; + +export default function PriceToggle({ + isAnnual, + onChange, +}: { + isAnnual: boolean; + onChange?: ChangeEventHandler; +}) { + return ( + <> + + + ); +} diff --git a/Frontend/sections/Payment.tsx b/Frontend/sections/Payment.tsx new file mode 100644 index 0000000..b058677 --- /dev/null +++ b/Frontend/sections/Payment.tsx @@ -0,0 +1,59 @@ +"use client"; + +import React, { useEffect } from "react"; +import { Product } from "@/types"; +import PriceCard from "@/components/PriceCard"; +import { useState } from "react"; +import PriceToggle from "@/components/PriceToggle"; +import { apiPrices } from "../app/(admin)/pricing/actions"; + +const Payment = ({ + type = "one_time", +}: { + type?: "one_time" | "reoccuring"; +}) => { + const [isAnnual, setIsAnnual] = useState(false); + const [products, setProducts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const handleToggle = () => { + setIsAnnual((prev) => !prev); + }; + useEffect(() => { + setIsLoading(true); + (async () => { + const resposeProducts: Product[] = await apiPrices(); + setProducts(resposeProducts); + setIsLoading(false); + })(); + }, []); + return ( + <> +
+ +
+
+
+ {isLoading ? ( + <> + + + + + ) : ( + products + .filter((x) => + type == "one_time" ? x.type == "one_time" : x.type != "one_time" + ) + .map((x, i) => ( + + )) + )} +
+
+ + ); +}; + +export default Payment; diff --git a/Frontend/types/index.ts b/Frontend/types/index.ts index 8340977..9372320 100644 --- a/Frontend/types/index.ts +++ b/Frontend/types/index.ts @@ -29,6 +29,7 @@ export type Product = { product_order: number; yearlyPrice: Price; monthlyPrice: Price; + type: string; } export type Price = {