Added packages for students to be able to purchase
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"countries-list": "^3.0.1",
|
"countries-list": "^3.0.1",
|
||||||
"country-codes-list": "^1.6.11",
|
"country-codes-list": "^1.6.11",
|
||||||
|
"currency-symbol-map": "^5.1.0",
|
||||||
"daisyui": "^3.1.5",
|
"daisyui": "^3.1.5",
|
||||||
"eslint": "8.33.0",
|
"eslint": "8.33.0",
|
||||||
"eslint-config-next": "13.1.6",
|
"eslint-config-next": "13.1.6",
|
||||||
|
|||||||
70
src/components/PayPalPayment.tsx
Normal file
70
src/components/PayPalPayment.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {DurationUnit} from "@/interfaces/paypal";
|
||||||
|
import {CreateOrderActions, CreateOrderData, OnApproveActions, OnApproveData, OnCancelledActions, OrderResponseBody} from "@paypal/paypal-js";
|
||||||
|
import {PayPalButtons, usePayPalScriptReducer} from "@paypal/react-paypal-js";
|
||||||
|
import axios from "axios";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currency: string;
|
||||||
|
price: number;
|
||||||
|
duration: number;
|
||||||
|
duration_unit: DurationUnit;
|
||||||
|
onSuccess: (duration: number, duration_unit: DurationUnit) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PayPalPayment({price, currency, duration, duration_unit, onSuccess}: Props) {
|
||||||
|
const [{options}, dispatch] = usePayPalScriptReducer();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({
|
||||||
|
type: "resetOptions",
|
||||||
|
value: {
|
||||||
|
...options,
|
||||||
|
currency,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currency]);
|
||||||
|
|
||||||
|
const createOrder = async (data: CreateOrderData, actions: CreateOrderActions): Promise<string> => {
|
||||||
|
console.log(data, actions);
|
||||||
|
|
||||||
|
return axios
|
||||||
|
.post<OrderResponseBody>("/api/paypal", {currencyCode: currency, price})
|
||||||
|
.then((response) => response.data)
|
||||||
|
.then((data) => data.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onApprove = async (data: OnApproveData, actions: OnApproveActions) => {
|
||||||
|
const request = await axios.post<{ok: boolean; reason?: string}>("/api/paypal/approve", {id: data.orderID, duration, duration_unit});
|
||||||
|
|
||||||
|
if (request.status !== 200) {
|
||||||
|
toast.error("Something went wrong, please try again later");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Your account has been credited more time!");
|
||||||
|
return onSuccess(duration, duration_unit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = async (data: Record<string, unknown>) => {
|
||||||
|
console.log(data);
|
||||||
|
toast.error("ERROR!");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = async (data: Record<string, unknown>, actions: OnCancelledActions) => {
|
||||||
|
console.log(data, actions);
|
||||||
|
toast.error("CANCEL!");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PayPalButtons
|
||||||
|
className="w-full"
|
||||||
|
style={{layout: "vertical"}}
|
||||||
|
createOrder={createOrder}
|
||||||
|
onApprove={onApprove}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onError={onError}></PayPalButtons>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
|
import PayPalPayment from "@/components/PayPalPayment";
|
||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
@@ -9,13 +10,16 @@ import useExamStore from "@/stores/examStore";
|
|||||||
import {getExamById} from "@/utils/exams";
|
import {getExamById} from "@/utils/exams";
|
||||||
import {MODULE_ARRAY, sortByModule, sortByModuleName} from "@/utils/moduleUtils";
|
import {MODULE_ARRAY, sortByModule, sortByModuleName} from "@/utils/moduleUtils";
|
||||||
import {averageScore, groupBySession} from "@/utils/stats";
|
import {averageScore, groupBySession} from "@/utils/stats";
|
||||||
|
import {CreateOrderActions, CreateOrderData, OnApproveActions, OnApproveData, OrderResponseBody} from "@paypal/paypal-js";
|
||||||
import {PayPalButtons} from "@paypal/react-paypal-js";
|
import {PayPalButtons} from "@paypal/react-paypal-js";
|
||||||
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {capitalize} from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
|
import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -84,11 +88,6 @@ export default function StudentDashboard({user}: Props) {
|
|||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="flex flex-col gap-1 md:gap-3">
|
|
||||||
<span className="font-bold text-lg">Payment</span>
|
|
||||||
<PayPalButtons style={{layout: "vertical"}} fundingSource={undefined}></PayPalButtons>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-1 md:gap-3">
|
<section className="flex flex-col gap-1 md:gap-3">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<div
|
<div
|
||||||
|
|||||||
22
src/hooks/usePackages.tsx
Normal file
22
src/hooks/usePackages.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {Exam} from "@/interfaces/exam";
|
||||||
|
import {Package} from "@/interfaces/paypal";
|
||||||
|
import axios from "axios";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
|
export default function usePackages() {
|
||||||
|
const [packages, setPackages] = useState<Package[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
axios
|
||||||
|
.get<Package[]>("/api/packages")
|
||||||
|
.then((response) => setPackages(response.data))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(getData, []);
|
||||||
|
|
||||||
|
return {packages, isLoading, isError, reload: getData};
|
||||||
|
}
|
||||||
23
src/interfaces/paypal.ts
Normal file
23
src/interfaces/paypal.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export interface TokenSuccess {
|
||||||
|
scope: string;
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
app_id: string;
|
||||||
|
expires_in: number;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenError {
|
||||||
|
error: string;
|
||||||
|
error_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Package {
|
||||||
|
id: string;
|
||||||
|
currency: string;
|
||||||
|
duration: number;
|
||||||
|
duration_unit: DurationUnit;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DurationUnit = "weeks" | "days" | "months" | "years";
|
||||||
@@ -22,6 +22,7 @@ export interface User {
|
|||||||
|
|
||||||
export interface CorporateInformation {
|
export interface CorporateInformation {
|
||||||
companyInformation: CompanyInformation;
|
companyInformation: CompanyInformation;
|
||||||
|
monthlyDuration: number;
|
||||||
payment?: {
|
payment?: {
|
||||||
value: number;
|
value: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default function RegisterCorporate({isLoading, setIsLoading, mutateUser,
|
|||||||
name: companyName,
|
name: companyName,
|
||||||
userAmount: companyUsers,
|
userAmount: companyUsers,
|
||||||
},
|
},
|
||||||
|
monthlyDuration: subscriptionDuration,
|
||||||
referralAgent,
|
referralAgent,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
137
src/pages/(status)/PaymentDue.tsx
Normal file
137
src/pages/(status)/PaymentDue.tsx
Normal 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' 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 />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ export default function App({Component, pageProps}: AppProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PayPalScriptProvider
|
<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} />
|
<Component {...pageProps} />
|
||||||
</PayPalScriptProvider>
|
</PayPalScriptProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
44
src/pages/api/packages/index.ts
Normal file
44
src/pages/api/packages/index.ts
Normal 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});
|
||||||
|
}
|
||||||
59
src/pages/api/paypal/approve.ts
Normal file
59
src/pages/api/paypal/approve.ts
Normal 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!"});
|
||||||
|
}
|
||||||
47
src/pages/api/paypal/index.ts
Normal file
47
src/pages/api/paypal/index.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ import AdminDashboard from "@/dashboards/Admin";
|
|||||||
import CorporateDashboard from "@/dashboards/Corporate";
|
import CorporateDashboard from "@/dashboards/Corporate";
|
||||||
import TeacherDashboard from "@/dashboards/Teacher";
|
import TeacherDashboard from "@/dashboards/Teacher";
|
||||||
import AgentDashboard from "@/dashboards/Agent";
|
import AgentDashboard from "@/dashboards/Agent";
|
||||||
|
import PaymentDue from "./(status)/PaymentDue";
|
||||||
|
import {useRouter} from "next/router";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
@@ -51,6 +53,8 @@ export default function Home() {
|
|||||||
const [showDiagnostics, setShowDiagnostics] = useState(false);
|
const [showDiagnostics, setShowDiagnostics] = useState(false);
|
||||||
const [showDemographicInput, setShowDemographicInput] = useState(false);
|
const [showDemographicInput, setShowDemographicInput] = useState(false);
|
||||||
const {user, mutateUser} = useUser({redirectTo: "/login"});
|
const {user, mutateUser} = useUser({redirectTo: "/login"});
|
||||||
|
const {stats} = useStats(user?.id);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -68,7 +72,7 @@ export default function Home() {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (user && (user.status === "disabled" || checkIfUserExpired())) {
|
if (user && (user.status === "paymentDue" || user.status === "disabled" || checkIfUserExpired())) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -80,34 +84,15 @@ export default function Home() {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Layout user={user} navDisabled>
|
{user.status === "disabled" && (
|
||||||
<div className="flex flex-col items-center justify-center text-center w-full gap-4">
|
<Layout user={user} navDisabled>
|
||||||
{user.status === "disabled" ? (
|
<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 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>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} />}
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
98
src/resources/paypal.ts
Normal file
98
src/resources/paypal.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
export const CURRENCIES: {label: string; currency: string}[] = [
|
||||||
|
{
|
||||||
|
label: "Australian dollar",
|
||||||
|
currency: "AUD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Brazilian real 2",
|
||||||
|
currency: "BRL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Canadian dollar",
|
||||||
|
currency: "CAD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Chinese Renmenbi 3",
|
||||||
|
currency: "CNY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Czech koruna",
|
||||||
|
currency: "CZK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Danish krone",
|
||||||
|
currency: "DKK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Euro",
|
||||||
|
currency: "EUR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hong Kong dollar",
|
||||||
|
currency: "HKD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hungarian forint 1",
|
||||||
|
currency: "HUF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Israeli new shekel",
|
||||||
|
currency: "ILS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Japanese yen 1",
|
||||||
|
currency: "JPY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Malaysian ringgit 3",
|
||||||
|
currency: "MYR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Mexican peso",
|
||||||
|
currency: "MXN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Taiwan dollar 1",
|
||||||
|
currency: "TWD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Zealand dollar",
|
||||||
|
currency: "NZD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Norwegian krone",
|
||||||
|
currency: "NOK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Philippine peso",
|
||||||
|
currency: "PHP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Polish złoty",
|
||||||
|
currency: "PLN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pound sterling",
|
||||||
|
currency: "GBP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Singapore dollar",
|
||||||
|
currency: "SGD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Swedish krona",
|
||||||
|
currency: "SEK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Swiss franc",
|
||||||
|
currency: "CHF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Thai baht",
|
||||||
|
currency: "THB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "United States dollar",
|
||||||
|
currency: "USD",
|
||||||
|
},
|
||||||
|
];
|
||||||
25
src/utils/paypal.ts
Normal file
25
src/utils/paypal.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {TokenError, TokenSuccess} from "@/interfaces/paypal";
|
||||||
|
import {base64} from "@firebase/util";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const getAccessToken = async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("grant_type", "client_credentials");
|
||||||
|
|
||||||
|
const auth = base64.encodeString(`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`);
|
||||||
|
|
||||||
|
const request = await axios
|
||||||
|
.post<TokenSuccess>(`${process.env.PAYPAL_ACCESS_TOKEN_URL}/v1/oauth2/token`, params, {
|
||||||
|
headers: {Authorization: `Basic ${auth}`},
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.data.access_token;
|
||||||
|
};
|
||||||
@@ -1949,6 +1949,11 @@ csstype@^3.0.2:
|
|||||||
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz"
|
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz"
|
||||||
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
||||||
|
|
||||||
|
currency-symbol-map@^5.1.0:
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/currency-symbol-map/-/currency-symbol-map-5.1.0.tgz#59531fbe977ba95e8d358e90e3c9e9053efb75ad"
|
||||||
|
integrity sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw==
|
||||||
|
|
||||||
daisyui@^3.1.5:
|
daisyui@^3.1.5:
|
||||||
version "3.5.1"
|
version "3.5.1"
|
||||||
resolved "https://registry.npmjs.org/daisyui/-/daisyui-3.5.1.tgz"
|
resolved "https://registry.npmjs.org/daisyui/-/daisyui-3.5.1.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user