Added packages for students to be able to purchase

This commit is contained in:
Tiago Ribeiro
2023-11-26 10:08:57 +00:00
parent c312260721
commit 7e91a989b3
16 changed files with 552 additions and 35 deletions

View File

@@ -67,6 +67,7 @@ export default function RegisterCorporate({isLoading, setIsLoading, mutateUser,
name: companyName,
userAmount: companyUsers,
},
monthlyDuration: subscriptionDuration,
referralAgent,
},
})

View File

@@ -0,0 +1,137 @@
/* eslint-disable @next/next/no-img-element */
import Layout from "@/components/High/Layout";
import PayPalPayment from "@/components/PayPalPayment";
import ProfileSummary from "@/components/ProfileSummary";
import useGroups from "@/hooks/useGroups";
import usePackages from "@/hooks/usePackages";
import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers";
import {Package} from "@/interfaces/paypal";
import {User} from "@/interfaces/user";
import {averageScore, groupBySession} from "@/utils/stats";
import clsx from "clsx";
import {capitalize} from "lodash";
import Head from "next/head";
import {useState} from "react";
import {BsFileEarmarkText, BsPencil, BsStar} from "react-icons/bs";
import getSymbolFromCurrency from "currency-symbol-map";
import {Divider} from "primereact/divider";
import Input from "@/components/Low/Input";
import Button from "@/components/Low/Button";
export default function PaymentDue({user, reload}: {user: User; reload: () => void}) {
const [selectedPackage, setPackage] = useState<string>();
const [code, setCode] = useState<string>();
const {packages, isLoading} = usePackages();
const {users} = useUsers();
const {groups} = useGroups();
const isIndividual = () => {
if (user?.type === "developer") return true;
if (user?.type !== "student") return false;
const userGroups = groups.filter((g) => g.participants.includes(user?.id));
if (userGroups.length === 0) return true;
const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t);
return userGroupsAdminTypes.every((t) => t !== "admin");
};
return user ? (
<Layout user={user} navDisabled>
<div className="flex flex-col items-center justify-center text-center w-full gap-4">
<span className="font-bold text-lg">You do not have time credits for your account type!</span>
{isIndividual() && (
<div className="flex flex-col items-center w-full overflow-x-scroll scrollbar-hide gap-12">
<span className="max-w-lg">To add to your use of EnCoach, please purchase one of the time packages available below:</span>
<div className="w-full flex flex-wrap justify-center gap-8">
{packages.map((p) => (
<div key={p.id} className={clsx("p-4 bg-white rounded-xl flex flex-col gap-6 items-start")}>
<div className="flex flex-col items-start mb-2">
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
<span className="font-semibold text-xl">
EnCoach - {p.duration}{" "}
{capitalize(p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit)}
</span>
</div>
<div className="flex flex-col gap-2 items-start w-full">
<span className="text-2xl">
{p.price}
{getSymbolFromCurrency(p.currency)}
</span>
<PayPalPayment
{...p}
onSuccess={(duration, duration_unit) => {
setTimeout(reload, 500);
}}
/>
</div>
<div className="flex flex-col gap-1 items-start">
<span>This includes:</span>
<ul className="flex flex-col items-start text-sm">
<li>- Train your abilities for the IELTS exam</li>
<li>- Gain insights into your weaknesses and strengths</li>
<li>- Allow yourself to correctly prepare for the exam</li>
</ul>
</div>
</div>
))}
</div>
</div>
)}
{!isIndividual() && user?.corporateInformation && user?.corporateInformation.payment && (
<div className="flex flex-col items-center">
<span className="max-w-lg">
To add to your use of EnCoach and that of your students and teachers, please pay your designated package below:
</span>
<div className={clsx("p-4 bg-white rounded-xl flex flex-col gap-6 items-start")}>
<div className="flex flex-col items-start mb-2">
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
<span className="font-semibold text-xl">EnCoach - {user.corporateInformation?.monthlyDuration} Month(s)</span>
</div>
<div className="flex flex-col gap-2 items-start w-full">
<span className="text-2xl">
{user.corporateInformation.payment.value}
{getSymbolFromCurrency(user.corporateInformation.payment.currency)}
</span>
<PayPalPayment
currency={user.corporateInformation.payment.currency}
price={user.corporateInformation.payment.value}
duration={user.corporateInformation.monthlyDuration}
duration_unit="months"
onSuccess={(duration, duration_unit) => {
setTimeout(reload, 500);
}}
/>
</div>
<div className="flex flex-col gap-1 items-start">
<span>This includes:</span>
<ul className="flex flex-col items-start text-sm">
<li>
- Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to use
EnCoach
</li>
<li>- Train their abilities for the IELTS exam</li>
<li>- Gain insights into your students&apos; weaknesses and strengths</li>
<li>- Allow them to correctly prepare for the exam</li>
</ul>
</div>
</div>
</div>
)}
{!isIndividual() && (!user?.corporateInformation || !user?.corporateInformation?.payment) && (
<div className="flex flex-col items-center">
<span className="max-w-lg">
An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users you desire
and your expected monthly duration.
</span>
<span className="max-w-lg">Please try again again later or contact your agent or an admin, thank you for your patience.</span>
</div>
)}
</div>
</Layout>
) : (
<div />
);
}

View File

@@ -34,7 +34,7 @@ export default function App({Component, pageProps}: AppProps) {
return (
<PayPalScriptProvider
options={{clientId: process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID || "", currency: "EUR", intent: "order", commit: true, vault: true}}>
options={{clientId: process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID || "", currency: "EUR", intent: "capture", commit: true, vault: true}}>
<Component {...pageProps} />
</PayPalScriptProvider>
);

View File

@@ -0,0 +1,44 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs, setDoc, doc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {Package} from "@/interfaces/paypal";
import {v4} from "uuid";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const snapshot = await getDocs(collection(db, "packages"));
res.status(200).json(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})),
);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!["developer", "owner"].includes(req.session.user!.type))
return res.status(403).json({ok: false, reason: "You do not have permission to create a new package"});
const body = req.body as Package;
await setDoc(doc(db, "packages", v4()), body);
res.status(200).json({ok: true});
}

View File

@@ -0,0 +1,59 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs, setDoc, doc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import axios from "axios";
import {DurationUnit, TokenError, TokenSuccess} from "@/interfaces/paypal";
import {base64} from "@firebase/util";
import {v4} from "uuid";
import {OrderResponseBody} from "@paypal/paypal-js";
import {getAccessToken} from "@/utils/paypal";
import moment from "moment";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(404).json({ok: false, reason: "Method not supported!"});
if (!req.session.user) return res.status(401).json({ok: false});
const accessToken = await getAccessToken();
if (!accessToken) return res.status(401).json({ok: false, reason: "Authorization failed!"});
const {id, duration, duration_unit} = req.body as {id: string; duration: number; duration_unit: DurationUnit};
const request = await axios.post(
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders/${id}/capture`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
if (request.data.status === "COMPLETED") {
const subscriptionExpirationDate = req.session.user.subscriptionExpirationDate;
const today = moment(new Date());
const dateToBeAddedTo = !subscriptionExpirationDate
? today
: moment(subscriptionExpirationDate).isAfter(today)
? moment(subscriptionExpirationDate)
: today;
const updatedExpirationDate = dateToBeAddedTo.add(duration, duration_unit);
await setDoc(
doc(db, "users", req.session.user.id),
{subscriptionExpirationDate: updatedExpirationDate.toISOString(), status: "active"},
{merge: true},
);
res.status(200).json({ok: true});
return;
}
res.status(404).json({ok: false, reason: "Order ID not found or purchase was not approved!"});
}

View File

@@ -0,0 +1,47 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import axios from "axios";
import {v4} from "uuid";
import {OrderResponseBody} from "@paypal/paypal-js";
import {getAccessToken} from "@/utils/paypal";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(404).json({ok: false, reason: "Method not supported!"});
if (!req.session.user) return res.status(401).json({ok: false});
const accessToken = await getAccessToken();
if (!accessToken) return res.status(401).json({ok: false, reason: "Authorization failed!"});
const {currencyCode, price} = req.body as {currencyCode: string; price: number};
const request = await axios.post<OrderResponseBody>(
`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders`,
{
purchase_units: [
{
amount: {
currency_code: currencyCode,
value: price.toString(),
},
reference_id: v4(),
},
],
intent: "CAPTURE",
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
res.status(request.status).json(request.data);
}

View File

@@ -27,6 +27,8 @@ import AdminDashboard from "@/dashboards/Admin";
import CorporateDashboard from "@/dashboards/Corporate";
import TeacherDashboard from "@/dashboards/Teacher";
import AgentDashboard from "@/dashboards/Agent";
import PaymentDue from "./(status)/PaymentDue";
import {useRouter} from "next/router";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
@@ -51,6 +53,8 @@ export default function Home() {
const [showDiagnostics, setShowDiagnostics] = useState(false);
const [showDemographicInput, setShowDemographicInput] = useState(false);
const {user, mutateUser} = useUser({redirectTo: "/login"});
const {stats} = useStats(user?.id);
const router = useRouter();
useEffect(() => {
if (user) {
@@ -68,7 +72,7 @@ export default function Home() {
return true;
};
if (user && (user.status === "disabled" || checkIfUserExpired())) {
if (user && (user.status === "paymentDue" || user.status === "disabled" || checkIfUserExpired())) {
return (
<>
<Head>
@@ -80,34 +84,15 @@ export default function Home() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Layout user={user} navDisabled>
<div className="flex flex-col items-center justify-center text-center w-full gap-4">
{user.status === "disabled" ? (
<>
<span className="font-bold text-lg">Your account has been disabled!</span>
<span>Please contact an administrator if you believe this to be a mistake.</span>
</>
) : (
<>
<span className="font-bold text-lg">Your subscription has expired!</span>
<div className="flex flex-col items-center">
<span>
Please purchase a new time pack{" "}
<Link
className="font-bold text-mti-purple-light underline hover:text-mti-purple-dark transition ease-in-out duration-300"
href="https://encoach.com/join">
here
</Link>
.
</span>
<span className="max-w-md">
If you are not the one in charge of your subscription, please contact the one responsible to extend it.
</span>
</div>
</>
)}
</div>
</Layout>
{user.status === "disabled" && (
<Layout user={user} navDisabled>
<div className="flex flex-col items-center justify-center text-center w-full gap-4">
<span className="font-bold text-lg">Your account has been disabled!</span>
<span>Please contact an administrator if you believe this to be a mistake.</span>
</div>
</Layout>
)}
{(user.status === "paymentDue" || checkIfUserExpired()) && <PaymentDue user={user} reload={router.reload} />}
</>
);
}